Fix AWG notation, add alternator type, migrate to String(localized:)
- Fix AWG 0/00/000/0000 bug (all resolved to 0 in Swift) using negative int convention (-1 through -4) with formatAWG() for 1/0–4/0 display - Add 7.5A fuse size and change fuse type from Int to Double - Add alternator power source type with distinct bolt.car.fill icon - Migrate all NSLocalizedString calls to String(localized:defaultValue:) - Update translations for runtime subtitle (ES/FR/NL: current→maximum), usable capacity footer text, and NL override wording - Store length always in meters, convert at display time in CalculatorView - Add preview-friendly inits for ComponentLibraryView and LoadsView - Expand test coverage for calculations, fuses, AWG, and edge cases Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -377,6 +377,7 @@
|
||||
"charger.source.solar" = "Solar";
|
||||
"charger.source.wind" = "Wind";
|
||||
"charger.source.generator" = "Generator";
|
||||
"charger.source.alternator" = "Alternator";
|
||||
|
||||
// MARK: - Share Menu
|
||||
"overview.share.diagram" = "Wiring Diagram";
|
||||
|
||||
@@ -166,11 +166,9 @@ struct BatteriesView: View {
|
||||
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"
|
||||
String(
|
||||
localized: "battery.bank.status.dismiss",
|
||||
defaultValue: "Got it"
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -516,12 +514,9 @@ struct BatteriesView: View {
|
||||
}
|
||||
|
||||
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"
|
||||
let format = String(
|
||||
localized: "battery.bank.empty.subtitle",
|
||||
defaultValue: "Tap the plus button to configure a battery for %@."
|
||||
)
|
||||
return String(format: format, systemName)
|
||||
}
|
||||
@@ -530,75 +525,59 @@ struct BatteriesView: View {
|
||||
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(
|
||||
localized: "battery.bank.status.single.battery",
|
||||
defaultValue: "One battery"
|
||||
)
|
||||
: String(
|
||||
format: NSLocalizedString(
|
||||
"battery.bank.status.multiple.batteries",
|
||||
bundle: .main,
|
||||
value: "%d batteries",
|
||||
comment: "Plural form describing mismatched battery count"
|
||||
format: String(
|
||||
localized: "battery.bank.status.multiple.batteries",
|
||||
defaultValue: "%d batteries"
|
||||
),
|
||||
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"
|
||||
format: String(
|
||||
localized: "battery.bank.status.voltage.message",
|
||||
defaultValue: "%@ diverges from the bank baseline of %@. Mixing nominal voltages leads to uneven charging and can damage connected chargers or inverters."
|
||||
),
|
||||
countText,
|
||||
expected
|
||||
)
|
||||
return (
|
||||
NSLocalizedString(
|
||||
"battery.bank.status.voltage.title",
|
||||
bundle: .main,
|
||||
value: "Voltage mismatch",
|
||||
comment: "Title for voltage mismatch alert"
|
||||
String(
|
||||
localized: "battery.bank.status.voltage.title",
|
||||
defaultValue: "Voltage mismatch"
|
||||
),
|
||||
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(
|
||||
localized: "battery.bank.status.single.battery",
|
||||
defaultValue: "One battery"
|
||||
)
|
||||
: String(
|
||||
format: NSLocalizedString(
|
||||
"battery.bank.status.multiple.batteries",
|
||||
bundle: .main,
|
||||
value: "%d batteries",
|
||||
comment: "Plural form describing mismatched battery count"
|
||||
format: String(
|
||||
localized: "battery.bank.status.multiple.batteries",
|
||||
defaultValue: "%d batteries"
|
||||
),
|
||||
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"
|
||||
format: String(
|
||||
localized: "battery.bank.status.capacity.message",
|
||||
defaultValue: "%@ uses a different capacity than the dominant bank size of %@. Mismatched capacities cause uneven discharge and premature wear."
|
||||
),
|
||||
countText,
|
||||
expected
|
||||
)
|
||||
return (
|
||||
NSLocalizedString(
|
||||
"battery.bank.status.capacity.title",
|
||||
bundle: .main,
|
||||
value: "Capacity mismatch",
|
||||
comment: "Title for capacity mismatch alert"
|
||||
String(
|
||||
localized: "battery.bank.status.capacity.title",
|
||||
defaultValue: "Capacity mismatch"
|
||||
),
|
||||
message
|
||||
)
|
||||
|
||||
@@ -108,245 +108,191 @@ struct BatteryEditorView: View {
|
||||
}
|
||||
|
||||
private var alertCancelTitle: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.cancel",
|
||||
bundle: .main,
|
||||
value: "Cancel",
|
||||
comment: "Cancel button title for edit alerts"
|
||||
String(
|
||||
localized: "battery.editor.alert.cancel",
|
||||
defaultValue: "Cancel"
|
||||
)
|
||||
}
|
||||
|
||||
private var alertSaveTitle: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.save",
|
||||
bundle: .main,
|
||||
value: "Save",
|
||||
comment: "Save button title for edit alerts"
|
||||
String(
|
||||
localized: "battery.editor.alert.save",
|
||||
defaultValue: "Save"
|
||||
)
|
||||
}
|
||||
|
||||
private var voltageAlertTitle: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.voltage.title",
|
||||
bundle: .main,
|
||||
value: "Edit Nominal Voltage",
|
||||
comment: "Title for the voltage edit alert"
|
||||
String(
|
||||
localized: "battery.editor.alert.voltage.title",
|
||||
defaultValue: "Edit Nominal Voltage"
|
||||
)
|
||||
}
|
||||
|
||||
private var voltageAlertPlaceholder: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.voltage.placeholder",
|
||||
bundle: .main,
|
||||
value: "Voltage",
|
||||
comment: "Placeholder for voltage text field"
|
||||
String(
|
||||
localized: "battery.editor.alert.voltage.placeholder",
|
||||
defaultValue: "Voltage"
|
||||
)
|
||||
}
|
||||
|
||||
private var voltageAlertMessage: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.voltage.message",
|
||||
bundle: .main,
|
||||
value: "Enter voltage in volts (V)",
|
||||
comment: "Message for the voltage edit alert"
|
||||
String(
|
||||
localized: "battery.editor.alert.voltage.message",
|
||||
defaultValue: "Enter voltage in volts (V)"
|
||||
)
|
||||
}
|
||||
|
||||
private var capacityAlertTitle: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.capacity.title",
|
||||
bundle: .main,
|
||||
value: "Edit Capacity",
|
||||
comment: "Title for the capacity edit alert"
|
||||
String(
|
||||
localized: "battery.editor.alert.capacity.title",
|
||||
defaultValue: "Edit Capacity"
|
||||
)
|
||||
}
|
||||
|
||||
private var capacityAlertPlaceholder: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.capacity.placeholder",
|
||||
bundle: .main,
|
||||
value: "Capacity",
|
||||
comment: "Placeholder for capacity text field"
|
||||
String(
|
||||
localized: "battery.editor.alert.capacity.placeholder",
|
||||
defaultValue: "Capacity"
|
||||
)
|
||||
}
|
||||
|
||||
private var capacityAlertMessage: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.capacity.message",
|
||||
bundle: .main,
|
||||
value: "Enter capacity in amp-hours (Ah)",
|
||||
comment: "Message for the capacity edit alert"
|
||||
String(
|
||||
localized: "battery.editor.alert.capacity.message",
|
||||
defaultValue: "Enter capacity in amp-hours (Ah)"
|
||||
)
|
||||
}
|
||||
|
||||
private var usableCapacityAlertTitle: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.usable_capacity.title",
|
||||
bundle: .main,
|
||||
value: "Edit Usable Capacity",
|
||||
comment: "Title for the usable capacity edit alert"
|
||||
String(
|
||||
localized: "battery.editor.alert.usable_capacity.title",
|
||||
defaultValue: "Edit Usable Capacity"
|
||||
)
|
||||
}
|
||||
|
||||
private var usableCapacityAlertPlaceholder: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.usable_capacity.placeholder",
|
||||
bundle: .main,
|
||||
value: "Usable Capacity (%)",
|
||||
comment: "Placeholder for the usable capacity text field"
|
||||
String(
|
||||
localized: "battery.editor.alert.usable_capacity.placeholder",
|
||||
defaultValue: "Usable Capacity (%)"
|
||||
)
|
||||
}
|
||||
|
||||
private var usableCapacityAlertMessage: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.usable_capacity.message",
|
||||
bundle: .main,
|
||||
value: "Enter usable capacity as a percentage (%)",
|
||||
comment: "Message for the usable capacity edit alert"
|
||||
String(
|
||||
localized: "battery.editor.alert.usable_capacity.message",
|
||||
defaultValue: "Enter usable capacity percentage (%)"
|
||||
)
|
||||
}
|
||||
|
||||
private var chargeVoltageAlertTitle: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.charge_voltage.title",
|
||||
bundle: .main,
|
||||
value: "Edit Charge Voltage",
|
||||
comment: "Title for the charge voltage edit alert"
|
||||
String(
|
||||
localized: "battery.editor.alert.charge_voltage.title",
|
||||
defaultValue: "Edit Charge Voltage"
|
||||
)
|
||||
}
|
||||
|
||||
private var chargeVoltageAlertPlaceholder: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.charge_voltage.placeholder",
|
||||
bundle: .main,
|
||||
value: "Charge Voltage",
|
||||
comment: "Placeholder for the charge voltage text field"
|
||||
String(
|
||||
localized: "battery.editor.alert.charge_voltage.placeholder",
|
||||
defaultValue: "Charge Voltage"
|
||||
)
|
||||
}
|
||||
|
||||
private var chargeVoltageAlertMessage: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.charge_voltage.message",
|
||||
bundle: .main,
|
||||
value: "Enter charge voltage in volts (V)",
|
||||
comment: "Message for the charge voltage edit alert"
|
||||
String(
|
||||
localized: "battery.editor.alert.charge_voltage.message",
|
||||
defaultValue: "Enter charge voltage in volts (V)"
|
||||
)
|
||||
}
|
||||
|
||||
private var cutOffVoltageAlertTitle: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.cutoff_voltage.title",
|
||||
bundle: .main,
|
||||
value: "Edit Cut-off Voltage",
|
||||
comment: "Title for the cut-off voltage edit alert"
|
||||
String(
|
||||
localized: "battery.editor.alert.cutoff_voltage.title",
|
||||
defaultValue: "Edit Cut-off Voltage"
|
||||
)
|
||||
}
|
||||
|
||||
private var cutOffVoltageAlertPlaceholder: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.cutoff_voltage.placeholder",
|
||||
bundle: .main,
|
||||
value: "Cut-off Voltage",
|
||||
comment: "Placeholder for the cut-off voltage text field"
|
||||
String(
|
||||
localized: "battery.editor.alert.cutoff_voltage.placeholder",
|
||||
defaultValue: "Cut-off Voltage"
|
||||
)
|
||||
}
|
||||
|
||||
private var cutOffVoltageAlertMessage: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.cutoff_voltage.message",
|
||||
bundle: .main,
|
||||
value: "Enter cut-off voltage in volts (V)",
|
||||
comment: "Message for the cut-off voltage edit alert"
|
||||
String(
|
||||
localized: "battery.editor.alert.cutoff_voltage.message",
|
||||
defaultValue: "Enter cut-off voltage in volts (V)"
|
||||
)
|
||||
}
|
||||
|
||||
private var chargeVoltageTitle: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.slider.charge_voltage",
|
||||
bundle: .main,
|
||||
value: "Charge Voltage",
|
||||
comment: "Title for the charge voltage slider"
|
||||
String(
|
||||
localized: "battery.editor.slider.charge_voltage",
|
||||
defaultValue: "Charge Voltage"
|
||||
)
|
||||
}
|
||||
|
||||
private var cutOffVoltageTitle: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.slider.cutoff_voltage",
|
||||
bundle: .main,
|
||||
value: "Cut-off Voltage",
|
||||
comment: "Title for the cut-off voltage slider"
|
||||
String(
|
||||
localized: "battery.editor.slider.cutoff_voltage",
|
||||
defaultValue: "Cut-off Voltage"
|
||||
)
|
||||
}
|
||||
|
||||
private var temperatureRangeTitle: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.slider.temperature_range",
|
||||
bundle: .main,
|
||||
value: "Temperature Range",
|
||||
comment: "Title for the temperature range editor"
|
||||
String(
|
||||
localized: "battery.editor.slider.temperature_range",
|
||||
defaultValue: "Temperature Range"
|
||||
)
|
||||
}
|
||||
|
||||
private var chargeVoltageHelperText: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.advanced.charge_voltage.helper",
|
||||
bundle: .main,
|
||||
value: "Set the maximum recommended charging voltage.",
|
||||
comment: "Helper text explaining charge voltage"
|
||||
String(
|
||||
localized: "battery.editor.advanced.charge_voltage.helper",
|
||||
defaultValue: "Set the maximum recommended charging voltage."
|
||||
)
|
||||
}
|
||||
|
||||
private var cutOffVoltageHelperText: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.advanced.cutoff_voltage.helper",
|
||||
bundle: .main,
|
||||
value: "Set the minimum safe discharge voltage.",
|
||||
comment: "Helper text explaining cut-off voltage"
|
||||
String(
|
||||
localized: "battery.editor.advanced.cutoff_voltage.helper",
|
||||
defaultValue: "Set the minimum safe discharge voltage."
|
||||
)
|
||||
}
|
||||
|
||||
private var temperatureRangeHelperText: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.advanced.temperature_range.helper",
|
||||
bundle: .main,
|
||||
value: "Define the recommended operating temperature range.",
|
||||
comment: "Helper text explaining temperature range"
|
||||
String(
|
||||
localized: "battery.editor.advanced.temperature_range.helper",
|
||||
defaultValue: "Define the recommended operating temperature range."
|
||||
)
|
||||
}
|
||||
|
||||
private var minimumTemperatureLabel: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.slider.temperature_range.min",
|
||||
bundle: .main,
|
||||
value: "Minimum",
|
||||
comment: "Label for minimum temperature control"
|
||||
String(
|
||||
localized: "battery.editor.slider.temperature_range.min",
|
||||
defaultValue: "Minimum"
|
||||
)
|
||||
}
|
||||
|
||||
private var maximumTemperatureLabel: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.slider.temperature_range.max",
|
||||
bundle: .main,
|
||||
value: "Maximum",
|
||||
comment: "Label for maximum temperature control"
|
||||
String(
|
||||
localized: "battery.editor.slider.temperature_range.max",
|
||||
defaultValue: "Maximum"
|
||||
)
|
||||
}
|
||||
|
||||
private var usableCapacityDefaultFooterFormat: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.advanced.usable_capacity.footer_default",
|
||||
bundle: .main,
|
||||
value: "Default value %@ based on chemistry.",
|
||||
comment: "Footer text explaining the default usable capacity value"
|
||||
String(
|
||||
localized: "battery.editor.advanced.usable_capacity.footer_default",
|
||||
defaultValue: "Defaults to %@ based on chemistry."
|
||||
)
|
||||
}
|
||||
|
||||
private var usableCapacityOverrideFooterFormat: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.advanced.usable_capacity.footer_override",
|
||||
bundle: .main,
|
||||
value: "Manual override active. Chemistry default remains %@.",
|
||||
comment: "Footer text explaining the usable capacity override"
|
||||
String(
|
||||
localized: "battery.editor.advanced.usable_capacity.footer_override",
|
||||
defaultValue: "Override active. Chemistry default remains %@."
|
||||
)
|
||||
}
|
||||
|
||||
@@ -388,29 +334,23 @@ struct BatteryEditorView: View {
|
||||
}
|
||||
|
||||
private var appearanceEditorTitle: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.appearance.title",
|
||||
bundle: .main,
|
||||
value: "Battery Appearance",
|
||||
comment: "Title for the battery appearance editor"
|
||||
String(
|
||||
localized: "battery.editor.appearance.title",
|
||||
defaultValue: "Battery Appearance"
|
||||
)
|
||||
}
|
||||
|
||||
private var appearanceEditorSubtitle: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.appearance.subtitle",
|
||||
bundle: .main,
|
||||
value: "Customize how this battery shows up",
|
||||
comment: "Subtitle shown in the battery appearance editor preview"
|
||||
String(
|
||||
localized: "battery.editor.appearance.subtitle",
|
||||
defaultValue: "Customize how this battery shows up"
|
||||
)
|
||||
}
|
||||
|
||||
private var appearanceAccessibilityLabel: String {
|
||||
NSLocalizedString(
|
||||
"battery.editor.appearance.accessibility",
|
||||
bundle: .main,
|
||||
value: "Edit battery appearance",
|
||||
comment: "Accessibility label for the battery appearance editor button"
|
||||
String(
|
||||
localized: "battery.editor.appearance.accessibility",
|
||||
defaultValue: "Edit battery appearance"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -527,11 +467,9 @@ struct BatteryEditorView: View {
|
||||
)
|
||||
}
|
||||
.alert(
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.minimum_temperature.title",
|
||||
bundle: .main,
|
||||
value: "Edit Minimum Temperature",
|
||||
comment: "Title for the minimum temperature edit alert"
|
||||
String(
|
||||
localized: "battery.editor.alert.minimum_temperature.title",
|
||||
defaultValue: "Edit Minimum Temperature"
|
||||
),
|
||||
isPresented: Binding(
|
||||
get: { temperatureEditingField == .minimumTemperature },
|
||||
@@ -544,11 +482,9 @@ struct BatteryEditorView: View {
|
||||
)
|
||||
) {
|
||||
TextField(
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.minimum_temperature.placeholder",
|
||||
bundle: .main,
|
||||
value: "Minimum Temperature (°C)",
|
||||
comment: "Placeholder for the minimum temperature text field"
|
||||
String(
|
||||
localized: "battery.editor.alert.minimum_temperature.placeholder",
|
||||
defaultValue: "Minimum Temperature (\u{00B0}C)"
|
||||
),
|
||||
text: $minimumTemperatureInput
|
||||
)
|
||||
@@ -564,11 +500,9 @@ struct BatteryEditorView: View {
|
||||
}
|
||||
|
||||
Button(
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.cancel",
|
||||
bundle: .main,
|
||||
value: "Cancel",
|
||||
comment: "Cancel button title for edit alerts"
|
||||
String(
|
||||
localized: "battery.editor.alert.cancel",
|
||||
defaultValue: "Cancel"
|
||||
),
|
||||
role: .cancel
|
||||
) {
|
||||
@@ -577,11 +511,9 @@ struct BatteryEditorView: View {
|
||||
}
|
||||
|
||||
Button(
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.save",
|
||||
bundle: .main,
|
||||
value: "Save",
|
||||
comment: "Save button title for edit alerts"
|
||||
String(
|
||||
localized: "battery.editor.alert.save",
|
||||
defaultValue: "Save"
|
||||
)
|
||||
) {
|
||||
if let parsed = parseInput(minimumTemperatureInput) {
|
||||
@@ -592,20 +524,16 @@ struct BatteryEditorView: View {
|
||||
}
|
||||
} message: {
|
||||
Text(
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.minimum_temperature.message",
|
||||
bundle: .main,
|
||||
value: "Enter minimum temperature in degrees Celsius (°C)",
|
||||
comment: "Message for the minimum temperature edit alert"
|
||||
String(
|
||||
localized: "battery.editor.alert.minimum_temperature.message",
|
||||
defaultValue: "Enter minimum temperature in degrees Celsius (\u{00B0}C)"
|
||||
)
|
||||
)
|
||||
}
|
||||
.alert(
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.maximum_temperature.title",
|
||||
bundle: .main,
|
||||
value: "Edit Maximum Temperature",
|
||||
comment: "Title for the maximum temperature edit alert"
|
||||
String(
|
||||
localized: "battery.editor.alert.maximum_temperature.title",
|
||||
defaultValue: "Edit Maximum Temperature"
|
||||
),
|
||||
isPresented: Binding(
|
||||
get: { temperatureEditingField == .maximumTemperature },
|
||||
@@ -618,11 +546,9 @@ struct BatteryEditorView: View {
|
||||
)
|
||||
) {
|
||||
TextField(
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.maximum_temperature.placeholder",
|
||||
bundle: .main,
|
||||
value: "Maximum Temperature (°C)",
|
||||
comment: "Placeholder for the maximum temperature text field"
|
||||
String(
|
||||
localized: "battery.editor.alert.maximum_temperature.placeholder",
|
||||
defaultValue: "Maximum Temperature (\u{00B0}C)"
|
||||
),
|
||||
text: $maximumTemperatureInput
|
||||
)
|
||||
@@ -638,11 +564,9 @@ struct BatteryEditorView: View {
|
||||
}
|
||||
|
||||
Button(
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.cancel",
|
||||
bundle: .main,
|
||||
value: "Cancel",
|
||||
comment: "Cancel button title for edit alerts"
|
||||
String(
|
||||
localized: "battery.editor.alert.cancel",
|
||||
defaultValue: "Cancel"
|
||||
),
|
||||
role: .cancel
|
||||
) {
|
||||
@@ -651,11 +575,9 @@ struct BatteryEditorView: View {
|
||||
}
|
||||
|
||||
Button(
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.save",
|
||||
bundle: .main,
|
||||
value: "Save",
|
||||
comment: "Save button title for edit alerts"
|
||||
String(
|
||||
localized: "battery.editor.alert.save",
|
||||
defaultValue: "Save"
|
||||
)
|
||||
) {
|
||||
if let parsed = parseInput(maximumTemperatureInput) {
|
||||
@@ -666,11 +588,9 @@ struct BatteryEditorView: View {
|
||||
}
|
||||
} message: {
|
||||
Text(
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.maximum_temperature.message",
|
||||
bundle: .main,
|
||||
value: "Enter maximum temperature in degrees Celsius (°C)",
|
||||
comment: "Message for the maximum temperature edit alert"
|
||||
String(
|
||||
localized: "battery.editor.alert.maximum_temperature.message",
|
||||
defaultValue: "Enter maximum temperature in degrees Celsius (\u{00B0}C)"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -120,29 +120,23 @@ struct ChargerEditorView: View {
|
||||
}
|
||||
|
||||
private var appearanceEditorTitle: String {
|
||||
NSLocalizedString(
|
||||
"charger.editor.appearance.title",
|
||||
bundle: .main,
|
||||
value: "Charger Appearance",
|
||||
comment: "Title for the charger appearance editor"
|
||||
String(
|
||||
localized: "charger.editor.appearance.title",
|
||||
defaultValue: "Charger Appearance"
|
||||
)
|
||||
}
|
||||
|
||||
private var appearanceEditorSubtitle: String {
|
||||
NSLocalizedString(
|
||||
"charger.editor.appearance.subtitle",
|
||||
bundle: .main,
|
||||
value: "Customize how this charger shows up",
|
||||
comment: "Subtitle shown in the charger appearance editor preview"
|
||||
String(
|
||||
localized: "charger.editor.appearance.subtitle",
|
||||
defaultValue: "Customize how this charger shows up"
|
||||
)
|
||||
}
|
||||
|
||||
private var appearanceAccessibilityLabel: String {
|
||||
NSLocalizedString(
|
||||
"charger.editor.appearance.accessibility",
|
||||
bundle: .main,
|
||||
value: "Edit charger appearance",
|
||||
comment: "Accessibility label for the charger appearance editor button"
|
||||
String(
|
||||
localized: "charger.editor.appearance.accessibility",
|
||||
defaultValue: "Edit charger appearance"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -195,92 +189,72 @@ struct ChargerEditorView: View {
|
||||
}
|
||||
|
||||
private var inputVoltageAlertTitle: String {
|
||||
NSLocalizedString(
|
||||
"charger.editor.alert.input_voltage.title",
|
||||
bundle: .main,
|
||||
value: "Edit Input Voltage",
|
||||
comment: "Title for the input voltage edit alert"
|
||||
String(
|
||||
localized: "charger.editor.alert.input_voltage.title",
|
||||
defaultValue: "Edit Input Voltage"
|
||||
)
|
||||
}
|
||||
|
||||
private var outputVoltageAlertTitle: String {
|
||||
NSLocalizedString(
|
||||
"charger.editor.alert.output_voltage.title",
|
||||
bundle: .main,
|
||||
value: "Edit Output Voltage",
|
||||
comment: "Title for the output voltage edit alert"
|
||||
String(
|
||||
localized: "charger.editor.alert.output_voltage.title",
|
||||
defaultValue: "Edit Output Voltage"
|
||||
)
|
||||
}
|
||||
|
||||
private var currentAlertTitle: String {
|
||||
NSLocalizedString(
|
||||
"charger.editor.alert.current.title",
|
||||
bundle: .main,
|
||||
value: "Edit Charge Current",
|
||||
comment: "Title for the charging current edit alert"
|
||||
String(
|
||||
localized: "charger.editor.alert.current.title",
|
||||
defaultValue: "Edit Charge Current"
|
||||
)
|
||||
}
|
||||
|
||||
private var powerAlertTitle: String {
|
||||
NSLocalizedString(
|
||||
"charger.editor.alert.power.title",
|
||||
bundle: .main,
|
||||
value: "Edit Charge Power",
|
||||
comment: "Title for the power edit alert"
|
||||
String(
|
||||
localized: "charger.editor.alert.power.title",
|
||||
defaultValue: "Edit Charge Power"
|
||||
)
|
||||
}
|
||||
|
||||
private var alertMessageVoltage: String {
|
||||
NSLocalizedString(
|
||||
"charger.editor.alert.voltage.message",
|
||||
bundle: .main,
|
||||
value: "Enter voltage in volts (V)",
|
||||
comment: "Message for voltage edit alerts"
|
||||
String(
|
||||
localized: "charger.editor.alert.voltage.message",
|
||||
defaultValue: "Enter voltage in volts (V)"
|
||||
)
|
||||
}
|
||||
|
||||
private var alertMessagePower: String {
|
||||
NSLocalizedString(
|
||||
"charger.editor.alert.power.message",
|
||||
bundle: .main,
|
||||
value: "Enter power in watts (W)",
|
||||
comment: "Message for the power edit alert"
|
||||
String(
|
||||
localized: "charger.editor.alert.power.message",
|
||||
defaultValue: "Enter power in watts (W)"
|
||||
)
|
||||
}
|
||||
|
||||
private var alertMessageCurrent: String {
|
||||
NSLocalizedString(
|
||||
"charger.editor.alert.current.message",
|
||||
bundle: .main,
|
||||
value: "Enter current in amps (A)",
|
||||
comment: "Message for the current edit alert"
|
||||
String(
|
||||
localized: "charger.editor.alert.current.message",
|
||||
defaultValue: "Enter current in amps (A)"
|
||||
)
|
||||
}
|
||||
|
||||
private var alertCancelTitle: String {
|
||||
NSLocalizedString(
|
||||
"charger.editor.alert.cancel",
|
||||
bundle: .main,
|
||||
value: "Cancel",
|
||||
comment: "Title for cancel buttons in edit alerts"
|
||||
String(
|
||||
localized: "charger.editor.alert.cancel",
|
||||
defaultValue: "Cancel"
|
||||
)
|
||||
}
|
||||
|
||||
private var alertSaveTitle: String {
|
||||
NSLocalizedString(
|
||||
"charger.editor.alert.save",
|
||||
bundle: .main,
|
||||
value: "Save",
|
||||
comment: "Title for save buttons in edit alerts"
|
||||
String(
|
||||
localized: "charger.editor.alert.save",
|
||||
defaultValue: "Save"
|
||||
)
|
||||
}
|
||||
|
||||
private var powerAlertPlaceholder: String {
|
||||
NSLocalizedString(
|
||||
"charger.editor.alert.power.placeholder",
|
||||
bundle: .main,
|
||||
value: "Power",
|
||||
comment: "Placeholder for the power edit alert"
|
||||
String(
|
||||
localized: "charger.editor.alert.power.placeholder",
|
||||
defaultValue: "Power"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ final class SavedCharger {
|
||||
case solar = "solar"
|
||||
case wind = "wind"
|
||||
case generator = "generator"
|
||||
case alternator = "alternator"
|
||||
|
||||
var id: Self { self }
|
||||
|
||||
@@ -34,6 +35,7 @@ final class SavedCharger {
|
||||
case .solar: return String(localized: "charger.source.solar", defaultValue: "Solar")
|
||||
case .wind: return String(localized: "charger.source.wind", defaultValue: "Wind")
|
||||
case .generator: return String(localized: "charger.source.generator", defaultValue: "Generator")
|
||||
case .alternator: return String(localized: "charger.source.alternator", defaultValue: "Alternator")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +45,7 @@ final class SavedCharger {
|
||||
case .solar: return "sun.max.fill"
|
||||
case .wind: return "wind"
|
||||
case .generator: return "engine.combustion.fill"
|
||||
case .alternator: return "bolt.car.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,9 +73,14 @@ class CableCalculator: ObservableObject {
|
||||
)
|
||||
}
|
||||
|
||||
var recommendedFuse: Int {
|
||||
var recommendedFuse: Double {
|
||||
ElectricalCalculations.recommendedFuse(forCurrent: current)
|
||||
}
|
||||
|
||||
var recommendedFuseFormatted: String {
|
||||
let fuse = recommendedFuse
|
||||
return fuse == fuse.rounded() ? String(format: "%.0f", fuse) : String(format: "%.1f", fuse)
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
|
||||
@@ -103,12 +103,12 @@ struct CalculatorView: View {
|
||||
.keyboardType(.decimalPad)
|
||||
.onAppear {
|
||||
if lengthInput.isEmpty {
|
||||
lengthInput = formattedValue(calculator.length)
|
||||
lengthInput = formattedValue(displayLength)
|
||||
}
|
||||
}
|
||||
.onChange(of: lengthInput) { _, newValue in
|
||||
guard editingValue == .length, let parsed = parseInput(newValue) else { return }
|
||||
calculator.length = roundToTenth(parsed)
|
||||
calculator.length = metersFromDisplayLength(roundToTenth(parsed))
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
editingValue = nil
|
||||
@@ -116,7 +116,7 @@ struct CalculatorView: View {
|
||||
}
|
||||
Button("Save") {
|
||||
if let parsed = parseInput(lengthInput) {
|
||||
calculator.length = roundToTenth(parsed)
|
||||
calculator.length = metersFromDisplayLength(roundToTenth(parsed))
|
||||
}
|
||||
editingValue = nil
|
||||
lengthInput = ""
|
||||
@@ -520,7 +520,7 @@ struct CalculatorView: View {
|
||||
.font(.caption2)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(calculator.recommendedFuse)A")
|
||||
Text("\(calculator.recommendedFuseFormatted)A")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.orange)
|
||||
@@ -535,11 +535,9 @@ struct CalculatorView: View {
|
||||
.font(.caption2)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
Text(String(format: unitSettings.unitSystem == .imperial ?
|
||||
"%.1fft @ %.0f AWG" :
|
||||
"%.1fm @ %.1fmm²",
|
||||
unitSettings.unitSystem == .imperial ? calculator.length * 3.28084 : calculator.length,
|
||||
calculator.crossSection(for: unitSettings.unitSystem)))
|
||||
Text(unitSettings.unitSystem == .imperial ?
|
||||
String(format: "%.1fft @ %@ AWG", displayLength, ElectricalCalculations.formatAWG(calculator.crossSection(for: unitSettings.unitSystem))) :
|
||||
String(format: "%.1fm @ %.1fmm²", displayLength, calculator.crossSection(for: unitSettings.unitSystem)))
|
||||
.font(.subheadline)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.blue)
|
||||
@@ -600,7 +598,7 @@ struct CalculatorView: View {
|
||||
.frame(height: 30)
|
||||
.frame(width:80)
|
||||
.overlay(
|
||||
Text("\(calculator.recommendedFuse)A")
|
||||
Text("\(calculator.recommendedFuseFormatted)A")
|
||||
.foregroundColor(.white)
|
||||
.fontWeight(.bold)
|
||||
)
|
||||
@@ -698,7 +696,7 @@ struct CalculatorView: View {
|
||||
Group {
|
||||
Button(action: {}) {
|
||||
Text(unitSettings.unitSystem == .imperial ?
|
||||
String(format: "%.0f AWG", calculator.crossSection(for: unitSettings.unitSystem)) :
|
||||
"\(ElectricalCalculations.formatAWG(calculator.crossSection(for: unitSettings.unitSystem))) AWG" :
|
||||
String(format: "%.1f mm²", calculator.crossSection(for: unitSettings.unitSystem)))
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.blue)
|
||||
@@ -708,7 +706,7 @@ struct CalculatorView: View {
|
||||
Text("•").foregroundColor(.secondary)
|
||||
|
||||
Button(action: { beginLengthEditing() }) {
|
||||
Text(String(format: "%.1f %@", calculator.length, unitSettings.unitSystem.lengthUnit))
|
||||
Text(String(format: "%.1f %@", displayLength, unitSettings.unitSystem.lengthUnit))
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
@@ -775,13 +773,9 @@ struct CalculatorView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
let descriptionKey = info.affiliateURL != nil
|
||||
? "affiliate.description.with_link"
|
||||
: "affiliate.description.without_link"
|
||||
let description = NSLocalizedString(
|
||||
descriptionKey,
|
||||
comment: "Explanation text beneath the affiliate button"
|
||||
)
|
||||
let description = info.affiliateURL != nil
|
||||
? String(localized: "affiliate.description.with_link", defaultValue: "Tapping above shows a full bill of materials before opening the affiliate link. Purchases may support VoltPlan.")
|
||||
: String(localized: "affiliate.description.without_link", defaultValue: "Tapping above shows a full bill of materials with shopping searches to help you source parts.")
|
||||
Text(description)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
@@ -801,7 +795,7 @@ struct CalculatorView: View {
|
||||
let unknownSizeLabel = String(localized: "bom.size.unknown", comment: "Fallback label when the cable size is unknown")
|
||||
if unitSystem == .imperial {
|
||||
if crossSectionValue > 0 {
|
||||
crossSectionLabel = String(format: "AWG %.0f", crossSectionValue)
|
||||
crossSectionLabel = "AWG \(ElectricalCalculations.formatAWG(crossSectionValue))"
|
||||
} else {
|
||||
crossSectionLabel = unknownSizeLabel
|
||||
}
|
||||
@@ -816,35 +810,29 @@ struct CalculatorView: View {
|
||||
let cableDetail = "\(lengthLabel) • \(crossSectionLabel)"
|
||||
let powerDetail = String(format: "%.0f W @ %.1f V", calculator.calculatedPower, calculator.voltage)
|
||||
|
||||
let fuseRating = calculator.recommendedFuse
|
||||
let fuseDetailFormat = NSLocalizedString(
|
||||
"bom.fuse.detail",
|
||||
comment: "Description for the fuse entry in the calculator BOM"
|
||||
)
|
||||
let fuseRating = calculator.recommendedFuseFormatted
|
||||
let fuseDetailFormat = String(localized: "bom.fuse.detail", defaultValue: "Inline holder and %@A fuse")
|
||||
let fuseDetail = String.localizedStringWithFormat(fuseDetailFormat, fuseRating)
|
||||
|
||||
let cableShoesDetailFormat = NSLocalizedString(
|
||||
"bom.terminals.detail",
|
||||
comment: "Description for the cable terminals entry in the calculator BOM"
|
||||
)
|
||||
let cableShoesDetailFormat = String(localized: "bom.terminals.detail", defaultValue: "Ring or spade terminals sized for %@ wiring")
|
||||
let cableShoesDetail = String.localizedStringWithFormat(cableShoesDetailFormat, crossSectionLabel.lowercased())
|
||||
|
||||
let cableGaugeQuery: String
|
||||
if unitSystem == .imperial {
|
||||
cableGaugeQuery = String(format: "AWG %.0f", crossSectionValue)
|
||||
cableGaugeQuery = "AWG \(ElectricalCalculations.formatAWG(crossSectionValue))"
|
||||
} else {
|
||||
cableGaugeQuery = String(format: "%.1f mm2", crossSectionValue)
|
||||
}
|
||||
|
||||
let redCableSearchFormat = NSLocalizedString("bom.search.cable.red", comment: "Amazon search query for red battery cable")
|
||||
let redCableSearchFormat = String(localized: "bom.search.cable.red", defaultValue: "%@ red battery cable")
|
||||
let redCableQuery = String(format: redCableSearchFormat, cableGaugeQuery)
|
||||
let blackCableSearchFormat = NSLocalizedString("bom.search.cable.black", comment: "Amazon search query for black battery cable")
|
||||
let blackCableSearchFormat = String(localized: "bom.search.cable.black", defaultValue: "%@ black battery cable")
|
||||
let blackCableQuery = String(format: blackCableSearchFormat, cableGaugeQuery)
|
||||
let fuseSearchFormat = NSLocalizedString("bom.search.fuse", comment: "Amazon search query for inline fuse holder")
|
||||
let fuseSearchFormat = String(localized: "bom.search.fuse", defaultValue: "inline fuse holder %@A")
|
||||
let fuseQuery = String(format: fuseSearchFormat, fuseRating)
|
||||
let terminalSearchFormat = NSLocalizedString("bom.search.terminals", comment: "Amazon search query for cable shoes")
|
||||
let terminalSearchFormat = String(localized: "bom.search.terminals", defaultValue: "%@ cable shoes")
|
||||
let terminalQuery = String(format: terminalSearchFormat, cableGaugeQuery)
|
||||
let deviceFallbackFormat = NSLocalizedString("bom.search.device.fallback", comment: "Amazon search query fallback for a DC device")
|
||||
let deviceFallbackFormat = String(localized: "bom.search.device.fallback", defaultValue: "DC device %.0fW %.0fV")
|
||||
let deviceQueryBase = calculator.loadName.isEmpty
|
||||
? String(format: deviceFallbackFormat, calculator.calculatedPower, calculator.voltage)
|
||||
: calculator.loadName
|
||||
@@ -1068,7 +1056,7 @@ struct CalculatorView: View {
|
||||
|
||||
private var lengthSliderRange: ClosedRange<Double> {
|
||||
let baseMax = unitSettings.unitSystem == .metric ? 20.0 : 60.0
|
||||
let upperBound = max(baseMax, calculator.length)
|
||||
let upperBound = max(baseMax, displayLength)
|
||||
return 0...upperBound
|
||||
}
|
||||
|
||||
@@ -1187,22 +1175,19 @@ struct CalculatorView: View {
|
||||
}
|
||||
|
||||
private var lengthSlider: some View {
|
||||
let lengthTitleFormat = NSLocalizedString(
|
||||
"slider.length.title",
|
||||
comment: "Title format for the cable length slider"
|
||||
)
|
||||
return SliderSection(title: String(format: lengthTitleFormat, locale: Locale.current, unitSettings.unitSystem.lengthUnit),
|
||||
let lengthTitleFormat = String(localized: "slider.length.title", defaultValue: "Cable Length (%@)")
|
||||
return SliderSection(title: String(format: lengthTitleFormat, locale: Locale.current, unitSettings.unitSystem.lengthUnit),
|
||||
value: Binding(
|
||||
get: { calculator.length },
|
||||
get: { displayLength },
|
||||
set: { newValue in
|
||||
if editingValue == .length {
|
||||
calculator.length = roundToTenth(newValue)
|
||||
calculator.length = metersFromDisplayLength(roundToTenth(newValue))
|
||||
} else {
|
||||
calculator.length = normalizedLength(for: newValue)
|
||||
calculator.length = metersFromDisplayLength(normalizedLength(for: newValue))
|
||||
}
|
||||
}
|
||||
),
|
||||
range: lengthSliderRange,
|
||||
),
|
||||
range: lengthSliderRange,
|
||||
unit: unitSettings.unitSystem.lengthUnit,
|
||||
tapAction: beginLengthEditing,
|
||||
snapValues: editingValue == .length ? nil : lengthSnapValues)
|
||||
@@ -1306,6 +1291,21 @@ struct CalculatorView: View {
|
||||
max(0, (value * 10).rounded() / 10)
|
||||
}
|
||||
|
||||
// MARK: - Length Unit Conversion (calculator.length is always in meters)
|
||||
|
||||
private static let metersToFeet = 3.28084
|
||||
private static let feetToMeters = 0.3048
|
||||
|
||||
/// Length in display units (meters or feet) for showing to the user
|
||||
private var displayLength: Double {
|
||||
unitSettings.unitSystem == .imperial ? calculator.length * Self.metersToFeet : calculator.length
|
||||
}
|
||||
|
||||
/// Convert a display-unit length (meters or feet) back to meters for storage
|
||||
private func metersFromDisplayLength(_ displayValue: Double) -> Double {
|
||||
unitSettings.unitSystem == .imperial ? displayValue * Self.feetToMeters : displayValue
|
||||
}
|
||||
|
||||
private func roundToNearestFive(_ value: Double) -> Double {
|
||||
max(0, (value / 5).rounded() * 5)
|
||||
}
|
||||
@@ -1342,7 +1342,7 @@ struct CalculatorView: View {
|
||||
}
|
||||
|
||||
private func beginLengthEditing() {
|
||||
lengthInput = formattedValue(calculator.length)
|
||||
lengthInput = formattedValue(displayLength)
|
||||
editingValue = .length
|
||||
}
|
||||
|
||||
@@ -1433,16 +1433,10 @@ private struct BillOfMaterialsView: View {
|
||||
let destinationURL = destinationURL(for: item)
|
||||
let accessibilityLabel: String = {
|
||||
if isCompleted {
|
||||
let format = NSLocalizedString(
|
||||
"bom.accessibility.mark.incomplete",
|
||||
comment: "Accessibility label to mark a BOM item incomplete"
|
||||
)
|
||||
let format = String(localized: "bom.accessibility.mark.incomplete", defaultValue: "Mark %@ incomplete")
|
||||
return String.localizedStringWithFormat(format, item.title)
|
||||
} else {
|
||||
let format = NSLocalizedString(
|
||||
"bom.accessibility.mark.complete",
|
||||
comment: "Accessibility label to mark a BOM item complete"
|
||||
)
|
||||
let format = String(localized: "bom.accessibility.mark.complete", defaultValue: "Mark %@ complete")
|
||||
return String.localizedStringWithFormat(format, item.title)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -211,6 +211,12 @@ final class ComponentLibraryViewModel: ObservableObject {
|
||||
self.urlSession = urlSession
|
||||
}
|
||||
|
||||
init(previewItems: [ComponentLibraryItem]) {
|
||||
self.urlSession = .shared
|
||||
self.items = previewItems
|
||||
self.isLoading = false
|
||||
}
|
||||
|
||||
func load() async {
|
||||
guard !isLoading else { return }
|
||||
isLoading = true
|
||||
@@ -528,9 +534,19 @@ final class ComponentLibraryViewModel: ObservableObject {
|
||||
|
||||
struct ComponentLibraryView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@StateObject private var viewModel = ComponentLibraryViewModel()
|
||||
@StateObject private var viewModel: ComponentLibraryViewModel
|
||||
@State private var searchText: String = ""
|
||||
let onSelect: (ComponentLibraryItem) -> Void
|
||||
|
||||
init(onSelect: @escaping (ComponentLibraryItem) -> Void) {
|
||||
self._viewModel = StateObject(wrappedValue: ComponentLibraryViewModel())
|
||||
self.onSelect = onSelect
|
||||
}
|
||||
|
||||
init(viewModel: ComponentLibraryViewModel, onSelect: @escaping (ComponentLibraryItem) -> Void) {
|
||||
self._viewModel = StateObject(wrappedValue: viewModel)
|
||||
self.onSelect = onSelect
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
|
||||
@@ -17,24 +17,26 @@ struct ElectricalCalculations {
|
||||
150.0, 185.0, 240.0, 300.0, 400.0, 500.0, 630.0,
|
||||
]
|
||||
|
||||
private static let standardAWG: [Int] = [20, 18, 16, 14, 12, 10, 8, 6, 4, 2, 1, 0, 00, 000, 0000]
|
||||
// Convention: 1/0 = -1, 2/0 = -2, 3/0 = -3, 4/0 = -4
|
||||
private static let standardAWG: [Int] = [20, 18, 16, 14, 12, 10, 8, 6, 4, 2, 1, -1, -2, -3, -4]
|
||||
private static let awgCrossSections: [Double] = [
|
||||
0.519, 0.823, 1.31, 2.08, 3.31, 5.26, 8.37, 13.3, 21.2, 33.6, 42.4, 53.5, 67.4, 85.0, 107.0,
|
||||
]
|
||||
|
||||
private static let standardFuses: [Int] = [
|
||||
1, 2, 3, 5, 7, 10, 15, 20, 25, 30, 35, 40, 50,
|
||||
private static let standardFuses: [Double] = [
|
||||
1, 2, 3, 5, 7.5, 10, 15, 20, 25, 30, 35, 40, 50,
|
||||
60, 70, 80, 100, 125, 150, 175, 200, 225, 250,
|
||||
300, 350, 400, 450, 500, 600, 700, 800,
|
||||
]
|
||||
|
||||
/// Length must always be in meters. unitSystem controls the output format (mm² vs AWG).
|
||||
static func recommendedCrossSection(
|
||||
length: Double,
|
||||
current: Double,
|
||||
voltage: Double,
|
||||
unitSystem: UnitSystem
|
||||
) -> Double {
|
||||
let lengthInMeters = unitSystem == .metric ? length : length * feetToMeters
|
||||
let lengthInMeters = length
|
||||
let maxVoltageDrop = voltage * maxVoltageDropFraction
|
||||
let minimumCrossSection = guardAgainstZero(maxVoltageDrop) {
|
||||
(2 * current * lengthInMeters * copperResistivity) / maxVoltageDrop
|
||||
@@ -65,7 +67,7 @@ struct ElectricalCalculations {
|
||||
unitSystem: unitSystem
|
||||
)
|
||||
|
||||
let lengthInMeters = unitSystem == .metric ? length : length * feetToMeters
|
||||
let lengthInMeters = length
|
||||
let crossSectionMM2: Double
|
||||
if unitSystem == .metric {
|
||||
crossSectionMM2 = selectedCrossSection
|
||||
@@ -112,8 +114,8 @@ struct ElectricalCalculations {
|
||||
return current * drop
|
||||
}
|
||||
|
||||
static func recommendedFuse(forCurrent current: Double) -> Int {
|
||||
let target = Int((current * 1.25).rounded(.up))
|
||||
static func recommendedFuse(forCurrent current: Double) -> Double {
|
||||
let target = (current * 1.25).rounded(.up)
|
||||
return standardFuses.first(where: { $0 >= target }) ?? standardFuses.last ?? target
|
||||
}
|
||||
|
||||
@@ -123,16 +125,22 @@ struct ElectricalCalculations {
|
||||
}
|
||||
|
||||
private static func crossSectionFromAWG(_ awg: Double) -> Double {
|
||||
switch awg {
|
||||
case 00: return 67.4
|
||||
case 000: return 85.0
|
||||
case 0000: return 107.0
|
||||
default:
|
||||
let index = standardAWG.firstIndex(of: Int(awg)) ?? -1
|
||||
if index >= 0 && index < awgCrossSections.count {
|
||||
return awgCrossSections[index]
|
||||
}
|
||||
return 0.75
|
||||
let index = standardAWG.firstIndex(of: Int(awg)) ?? -1
|
||||
if index >= 0 && index < awgCrossSections.count {
|
||||
return awgCrossSections[index]
|
||||
}
|
||||
return 0.75
|
||||
}
|
||||
|
||||
/// Formats an AWG value for display: positive values as-is, negative as "X/0" notation.
|
||||
static func formatAWG(_ awg: Double) -> String {
|
||||
let intAWG = Int(awg)
|
||||
switch intAWG {
|
||||
case -1: return "1/0"
|
||||
case -2: return "2/0"
|
||||
case -3: return "3/0"
|
||||
case -4: return "4/0"
|
||||
default: return "\(intAWG)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,9 @@ enum LoadConfigurationStatus: Identifiable, Equatable {
|
||||
var bannerText: String {
|
||||
switch self {
|
||||
case .missingDetails:
|
||||
return NSLocalizedString(
|
||||
"loads.overview.status.missing_details.banner",
|
||||
bundle: .main,
|
||||
value: "Finish configuring your loads",
|
||||
comment: "Short banner text describing loads that need additional details"
|
||||
return String(
|
||||
localized: "loads.overview.status.missing_details.banner",
|
||||
defaultValue: "Finish configuring your loads"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -39,30 +37,22 @@ enum LoadConfigurationStatus: Identifiable, Equatable {
|
||||
func detailInfo() -> LoadStatusDetail {
|
||||
switch self {
|
||||
case .missingDetails(let count):
|
||||
let title = NSLocalizedString(
|
||||
"loads.overview.status.missing_details.title",
|
||||
bundle: .main,
|
||||
value: "Missing load details",
|
||||
comment: "Alert title when loads are missing required details"
|
||||
let title = String(
|
||||
localized: "loads.overview.status.missing_details.title",
|
||||
defaultValue: "Missing load details"
|
||||
)
|
||||
let format = NSLocalizedString(
|
||||
"loads.overview.status.missing_details.message",
|
||||
bundle: .main,
|
||||
value: "Enter cable length and wire size for %d %@ to see accurate recommendations.",
|
||||
comment: "Alert message when loads are missing required details"
|
||||
let format = String(
|
||||
localized: "loads.overview.status.missing_details.message",
|
||||
defaultValue: "Enter cable length and wire size for %d %@ to see accurate recommendations."
|
||||
)
|
||||
let loadWord = count == 1
|
||||
? NSLocalizedString(
|
||||
"loads.overview.status.missing_details.singular",
|
||||
bundle: .main,
|
||||
value: "load",
|
||||
comment: "Singular noun for load"
|
||||
? String(
|
||||
localized: "loads.overview.status.missing_details.singular",
|
||||
defaultValue: "load"
|
||||
)
|
||||
: NSLocalizedString(
|
||||
"loads.overview.status.missing_details.plural",
|
||||
bundle: .main,
|
||||
value: "loads",
|
||||
comment: "Plural noun for loads"
|
||||
: String(
|
||||
localized: "loads.overview.status.missing_details.plural",
|
||||
defaultValue: "loads"
|
||||
)
|
||||
let message = String(format: format, count, loadWord)
|
||||
return LoadStatusDetail(title: title, message: message)
|
||||
|
||||
@@ -21,7 +21,7 @@ struct LoadsView: View {
|
||||
@State private var hasOpenedLoadOnAppear = false
|
||||
@State private var showingComponentLibrary = false
|
||||
@State private var showingSystemBOM = false
|
||||
@State private var selectedComponentTab: ComponentTab = .overview
|
||||
@State private var selectedComponentTab: ComponentTab
|
||||
@State private var batteryDraft: BatteryConfiguration?
|
||||
@State private var chargerDraft: ChargerConfiguration?
|
||||
@State private var activeStatus: LoadConfigurationStatus?
|
||||
@@ -36,10 +36,11 @@ struct LoadsView: View {
|
||||
private let presentSystemEditorOnAppear: Bool
|
||||
private let loadToOpenOnAppear: SavedLoad?
|
||||
|
||||
init(system: ElectricalSystem, presentSystemEditorOnAppear: Bool = false, loadToOpenOnAppear: SavedLoad? = nil) {
|
||||
init(system: ElectricalSystem, presentSystemEditorOnAppear: Bool = false, loadToOpenOnAppear: SavedLoad? = nil, initialTab: ComponentTab = .overview) {
|
||||
self.system = system
|
||||
self.presentSystemEditorOnAppear = presentSystemEditorOnAppear
|
||||
self.loadToOpenOnAppear = loadToOpenOnAppear
|
||||
self._selectedComponentTab = State(initialValue: initialTab)
|
||||
}
|
||||
|
||||
private var savedLoads: [SavedLoad] {
|
||||
@@ -312,12 +313,7 @@ struct LoadsView: View {
|
||||
message: Text(detail.message),
|
||||
dismissButton: .default(
|
||||
Text(
|
||||
NSLocalizedString(
|
||||
"battery.bank.status.dismiss",
|
||||
bundle: .main,
|
||||
value: "Got it",
|
||||
comment: "Dismiss button title for load status alert"
|
||||
)
|
||||
String(localized: "battery.bank.status.dismiss", defaultValue: "Got it")
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -610,7 +606,7 @@ struct LoadsView: View {
|
||||
private func wireGaugeString(for load: SavedLoad) -> String {
|
||||
if unitSettings.unitSystem == .imperial {
|
||||
let awgValue = awgFromCrossSection(load.crossSection)
|
||||
return String(format: "%.0f AWG", awgValue)
|
||||
return "\(ElectricalCalculations.formatAWG(awgValue)) AWG"
|
||||
} else {
|
||||
return String(format: "%.1f mm²", load.crossSection)
|
||||
}
|
||||
@@ -679,66 +675,31 @@ struct LoadsView: View {
|
||||
}
|
||||
|
||||
private var fuseMetricLabel: String {
|
||||
NSLocalizedString(
|
||||
"loads.metric.fuse",
|
||||
bundle: .main,
|
||||
value: "Fuse",
|
||||
comment: "Label for fuse metric in load detail row"
|
||||
)
|
||||
String(localized: "loads.metric.fuse", defaultValue: "Fuse")
|
||||
}
|
||||
|
||||
private var cableMetricLabel: String {
|
||||
NSLocalizedString(
|
||||
"loads.metric.cable",
|
||||
bundle: .main,
|
||||
value: "Cable",
|
||||
comment: "Label for cable metric in load detail row"
|
||||
)
|
||||
String(localized: "loads.metric.cable", defaultValue: "Cable")
|
||||
}
|
||||
|
||||
private var lengthMetricLabel: String {
|
||||
NSLocalizedString(
|
||||
"loads.metric.length",
|
||||
bundle: .main,
|
||||
value: "Length",
|
||||
comment: "Label for cable length metric in load detail row"
|
||||
)
|
||||
String(localized: "loads.metric.length", defaultValue: "Length")
|
||||
}
|
||||
|
||||
private var loadsSummaryTitle: String {
|
||||
NSLocalizedString(
|
||||
"loads.overview.header.title",
|
||||
bundle: .main,
|
||||
value: "Load Overview",
|
||||
comment: "Title for the loads overview summary section"
|
||||
)
|
||||
String(localized: "loads.overview.header.title", defaultValue: "Load Overview")
|
||||
}
|
||||
|
||||
private var loadsCountLabel: String {
|
||||
NSLocalizedString(
|
||||
"loads.overview.metric.count",
|
||||
bundle: .main,
|
||||
value: "Loads",
|
||||
comment: "Label for number of loads metric"
|
||||
)
|
||||
String(localized: "loads.overview.metric.count", defaultValue: "Loads")
|
||||
}
|
||||
|
||||
private var loadsCurrentLabel: String {
|
||||
NSLocalizedString(
|
||||
"loads.overview.metric.current",
|
||||
bundle: .main,
|
||||
value: "Total Current",
|
||||
comment: "Label for total load current metric"
|
||||
)
|
||||
String(localized: "loads.overview.metric.current", defaultValue: "Total Current")
|
||||
}
|
||||
|
||||
private var loadsPowerLabel: String {
|
||||
NSLocalizedString(
|
||||
"loads.overview.metric.power",
|
||||
bundle: .main,
|
||||
value: "Total Power",
|
||||
comment: "Label for total load power metric"
|
||||
)
|
||||
String(localized: "loads.overview.metric.power", defaultValue: "Total Power")
|
||||
}
|
||||
|
||||
private var totalCurrent: Double {
|
||||
@@ -1047,20 +1008,21 @@ struct LoadsView: View {
|
||||
}
|
||||
|
||||
private func awgFromCrossSection(_ crossSectionMM2: Double) -> Double {
|
||||
let awgSizes = [(20, 0.519), (18, 0.823), (16, 1.31), (14, 2.08), (12, 3.31),
|
||||
(10, 5.26), (8, 8.37), (6, 13.3), (4, 21.2), (2, 33.6),
|
||||
(1, 42.4), (0, 53.5), (00, 67.4), (000, 85.0), (0000, 107.0)]
|
||||
|
||||
let awgSizes: [(Int, Double)] = [(20, 0.519), (18, 0.823), (16, 1.31), (14, 2.08), (12, 3.31),
|
||||
(10, 5.26), (8, 8.37), (6, 13.3), (4, 21.2), (2, 33.6),
|
||||
(1, 42.4), (-1, 53.5), (-2, 67.4), (-3, 85.0), (-4, 107.0)]
|
||||
|
||||
// Find the closest AWG size
|
||||
let closest = awgSizes.min { abs($0.1 - crossSectionMM2) < abs($1.1 - crossSectionMM2) }
|
||||
return Double(closest?.0 ?? 20)
|
||||
}
|
||||
|
||||
private func recommendedFuse(for load: SavedLoad) -> Int {
|
||||
ElectricalCalculations.recommendedFuse(forCurrent: load.current)
|
||||
|
||||
private func recommendedFuse(for load: SavedLoad) -> String {
|
||||
let fuse = ElectricalCalculations.recommendedFuse(forCurrent: load.current)
|
||||
return fuse == fuse.rounded() ? String(format: "%.0f", fuse) : String(format: "%.1f", fuse)
|
||||
}
|
||||
|
||||
private enum ComponentTab: Hashable {
|
||||
enum ComponentTab: Hashable {
|
||||
case overview
|
||||
case components
|
||||
case batteries
|
||||
@@ -1146,19 +1108,6 @@ struct LoadsView: View {
|
||||
UIRectFill(CGRect(origin: .zero, size: size))
|
||||
image.draw(at: .zero)
|
||||
|
||||
// Draw app icon as subtle watermark in bottom-right corner
|
||||
if let appIcon = UIImage(named: "AppIconWatermark") {
|
||||
let watermarkSize: CGFloat = min(size.width, size.height) * 0.08
|
||||
let margin: CGFloat = watermarkSize * 0.4
|
||||
let watermarkRect = CGRect(
|
||||
x: size.width - watermarkSize - margin,
|
||||
y: size.height - watermarkSize - margin,
|
||||
width: watermarkSize,
|
||||
height: watermarkSize
|
||||
)
|
||||
appIcon.draw(in: watermarkRect, blendMode: .normal, alpha: 0.15)
|
||||
}
|
||||
|
||||
return UIGraphicsGetImageFromCurrentImageContext()
|
||||
}
|
||||
|
||||
@@ -1268,3 +1217,4 @@ struct LoadsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ struct SystemOverviewPDFExporter {
|
||||
let voltageDrop: Double
|
||||
let voltageDropPercent: Double
|
||||
let powerLoss: Double
|
||||
let recommendedFuse: Int
|
||||
let recommendedFuse: Double
|
||||
let iconUrl: String?
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ struct SystemOverviewPDFExporter {
|
||||
static func fetchDiagramImage(snapshot: SystemSnapshot) async -> UIImage? {
|
||||
let payload: [String: Any] = [
|
||||
"systemName": snapshot.systemName,
|
||||
"source": "cable",
|
||||
"unitSystem": snapshot.unitSystem == .metric ? "metric" : "imperial",
|
||||
"loads": snapshot.loads.map { load in
|
||||
var dict: [String: Any] = [
|
||||
@@ -777,7 +778,7 @@ struct SystemOverviewPDFExporter {
|
||||
(voltageLabel, "\(formatNumber(load.voltage)) V", currentLabel, "\(formatNumber(load.current)) A"),
|
||||
(powerLabel, formatPower(load.power), lengthLabel, "\(formatNumber(load.length)) \(lengthUnit)"),
|
||||
(cableLabel, "\(formatNumber(load.recommendedCrossSection)) \(wireUnit)", vDropLabel, "\(formatNumber(load.voltageDropPercent))%"),
|
||||
(pLossLabel, formatPower(load.powerLoss), fuseLabel, "\(load.recommendedFuse) A"),
|
||||
(pLossLabel, formatPower(load.powerLoss), fuseLabel, "\(formatNumber(load.recommendedFuse)) A"),
|
||||
]
|
||||
|
||||
for row in rows {
|
||||
|
||||
@@ -398,11 +398,9 @@ struct SystemOverviewView: View {
|
||||
message: Text(detail.message),
|
||||
dismissButton: .default(
|
||||
Text(
|
||||
NSLocalizedString(
|
||||
"battery.bank.status.dismiss",
|
||||
bundle: .main,
|
||||
value: "Got it",
|
||||
comment: "Dismiss button title for load status alert"
|
||||
String(
|
||||
localized: "battery.bank.status.dismiss",
|
||||
defaultValue: "Got it"
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -962,266 +960,123 @@ struct SystemOverviewView: View {
|
||||
}
|
||||
|
||||
private var loadsSummaryTitle: String {
|
||||
NSLocalizedString(
|
||||
"loads.overview.header.title",
|
||||
bundle: .main,
|
||||
value: "Load Overview",
|
||||
comment: "Title for the loads overview summary section"
|
||||
)
|
||||
String(localized: "loads.overview.header.title", defaultValue: "Load Overview")
|
||||
}
|
||||
|
||||
private var loadsCountLabel: String {
|
||||
NSLocalizedString(
|
||||
"loads.overview.metric.count",
|
||||
bundle: .main,
|
||||
value: "Loads",
|
||||
comment: "Label for number of loads metric"
|
||||
)
|
||||
String(localized: "loads.overview.metric.count", defaultValue: "Loads")
|
||||
}
|
||||
|
||||
private var loadsCurrentLabel: String {
|
||||
NSLocalizedString(
|
||||
"loads.overview.metric.current",
|
||||
bundle: .main,
|
||||
value: "Total Current",
|
||||
comment: "Label for total load current metric"
|
||||
)
|
||||
String(localized: "loads.overview.metric.current", defaultValue: "Total Current")
|
||||
}
|
||||
|
||||
private var loadsPowerLabel: String {
|
||||
NSLocalizedString(
|
||||
"loads.overview.metric.power",
|
||||
bundle: .main,
|
||||
value: "Total Power",
|
||||
comment: "Label for total load power metric"
|
||||
)
|
||||
String(localized: "loads.overview.metric.power", defaultValue: "Total Power")
|
||||
}
|
||||
|
||||
private var loadsEmptyTitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.loads.empty.title",
|
||||
bundle: .main,
|
||||
value: "No loads configured yet",
|
||||
comment: "Title shown in overview when no loads exist"
|
||||
)
|
||||
String(localized: "overview.loads.empty.title", defaultValue: "No loads configured yet")
|
||||
}
|
||||
|
||||
private var loadsEmptySubtitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.loads.empty.subtitle",
|
||||
bundle: .main,
|
||||
value: "Add components to get cable sizing and fuse recommendations tailored to this system.",
|
||||
comment: "Subtitle shown in overview when no loads exist"
|
||||
)
|
||||
String(localized: "overview.loads.empty.subtitle", defaultValue: "Add components to get cable sizing and fuse recommendations tailored to this system.")
|
||||
}
|
||||
|
||||
private var loadsEmptyMessage: String {
|
||||
NSLocalizedString(
|
||||
"loads.overview.empty.message",
|
||||
bundle: .main,
|
||||
value: "Start by adding a load to see system insights.",
|
||||
comment: "Message shown when no loads exist"
|
||||
)
|
||||
String(localized: "loads.overview.empty.message", defaultValue: "Start by adding a load to see system insights.")
|
||||
}
|
||||
|
||||
private var loadsEmptyCreateAction: String {
|
||||
NSLocalizedString(
|
||||
"loads.overview.empty.create",
|
||||
bundle: .main,
|
||||
value: "Create Load",
|
||||
comment: "Button title to create a new load"
|
||||
)
|
||||
String(localized: "loads.overview.empty.create", defaultValue: "Add Load")
|
||||
}
|
||||
|
||||
private var loadsEmptyBrowseAction: String {
|
||||
NSLocalizedString(
|
||||
"loads.overview.empty.library",
|
||||
bundle: .main,
|
||||
value: "Browse Library",
|
||||
comment: "Button title to open load library"
|
||||
)
|
||||
String(localized: "loads.overview.empty.library", defaultValue: "Browse Library")
|
||||
}
|
||||
|
||||
private var batterySummaryTitle: String {
|
||||
NSLocalizedString(
|
||||
"battery.bank.header.title",
|
||||
bundle: .main,
|
||||
value: "Battery Bank",
|
||||
comment: "Title for the battery bank summary section"
|
||||
)
|
||||
String(localized: "battery.bank.header.title", defaultValue: "Battery Bank")
|
||||
}
|
||||
|
||||
private var batteryCountLabel: String {
|
||||
NSLocalizedString(
|
||||
"battery.bank.metric.count",
|
||||
bundle: .main,
|
||||
value: "Batteries",
|
||||
comment: "Label for number of batteries metric"
|
||||
)
|
||||
String(localized: "battery.bank.metric.count", defaultValue: "Batteries")
|
||||
}
|
||||
|
||||
private var batteryCapacityLabel: String {
|
||||
NSLocalizedString(
|
||||
"battery.bank.metric.capacity",
|
||||
bundle: .main,
|
||||
value: "Capacity",
|
||||
comment: "Label for total capacity metric"
|
||||
)
|
||||
String(localized: "battery.bank.metric.capacity", defaultValue: "Capacity")
|
||||
}
|
||||
|
||||
private var batteryUsableCapacityLabel: String {
|
||||
NSLocalizedString(
|
||||
"battery.bank.metric.usable_capacity",
|
||||
bundle: .main,
|
||||
value: "Usable Capacity",
|
||||
comment: "Label for usable capacity metric"
|
||||
)
|
||||
String(localized: "battery.bank.metric.usable_capacity", defaultValue: "Usable Capacity")
|
||||
}
|
||||
|
||||
private var batteryUsableEnergyLabel: String {
|
||||
NSLocalizedString(
|
||||
"battery.bank.metric.usable_energy",
|
||||
bundle: .main,
|
||||
value: "Usable Energy",
|
||||
comment: "Label for usable energy metric"
|
||||
)
|
||||
String(localized: "battery.bank.metric.usable_energy", defaultValue: "Usable Energy")
|
||||
}
|
||||
|
||||
private var batteryEmptyTitle: String {
|
||||
NSLocalizedString(
|
||||
"battery.bank.empty.title",
|
||||
bundle: .main,
|
||||
value: "No Batteries Yet",
|
||||
comment: "Title shown when no batteries are configured"
|
||||
)
|
||||
String(localized: "battery.bank.empty.title", defaultValue: "No Batteries Yet")
|
||||
}
|
||||
|
||||
private var batteryEmptySubtitle: 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"
|
||||
let format = String(
|
||||
localized: "battery.bank.empty.subtitle",
|
||||
defaultValue: "Tap the plus button to configure a battery for %@."
|
||||
)
|
||||
return String(format: format, system.name)
|
||||
}
|
||||
|
||||
private var batteryEmptyCreateAction: String {
|
||||
NSLocalizedString(
|
||||
"battery.overview.empty.create",
|
||||
bundle: .main,
|
||||
value: "Create Battery",
|
||||
comment: "Button title to create a new battery"
|
||||
)
|
||||
String(localized: "battery.overview.empty.create", defaultValue: "Add Battery")
|
||||
}
|
||||
|
||||
private var chargerSummaryTitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.chargers.header.title",
|
||||
bundle: .main,
|
||||
value: "Charger Overview",
|
||||
comment: "Title for the chargers summary section"
|
||||
)
|
||||
String(localized: "overview.chargers.header.title", defaultValue: "Charger Overview")
|
||||
}
|
||||
|
||||
private var chargerCountLabel: String {
|
||||
NSLocalizedString(
|
||||
"chargers.summary.metric.count",
|
||||
bundle: .main,
|
||||
value: "Chargers",
|
||||
comment: "Label for number of chargers metric"
|
||||
)
|
||||
String(localized: "chargers.summary.metric.count", defaultValue: "Chargers")
|
||||
}
|
||||
|
||||
private var chargerOutputLabel: String {
|
||||
NSLocalizedString(
|
||||
"chargers.summary.metric.output",
|
||||
bundle: .main,
|
||||
value: "Output Voltage",
|
||||
comment: "Label for representative output voltage metric"
|
||||
)
|
||||
String(localized: "chargers.summary.metric.output", defaultValue: "Output Voltage")
|
||||
}
|
||||
|
||||
private var chargerCurrentLabel: String {
|
||||
NSLocalizedString(
|
||||
"chargers.summary.metric.current",
|
||||
bundle: .main,
|
||||
value: "Charge Rate",
|
||||
comment: "Label for total charger current metric"
|
||||
)
|
||||
String(localized: "chargers.summary.metric.current", defaultValue: "Charge Rate")
|
||||
}
|
||||
|
||||
private var chargerPowerLabel: String {
|
||||
NSLocalizedString(
|
||||
"chargers.summary.metric.power",
|
||||
bundle: .main,
|
||||
value: "Charge Power",
|
||||
comment: "Label for total charger power metric"
|
||||
)
|
||||
String(localized: "chargers.summary.metric.power", defaultValue: "Charge Power")
|
||||
}
|
||||
|
||||
private var chargerEmptyTitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.chargers.empty.title",
|
||||
bundle: .main,
|
||||
value: "No chargers configured yet",
|
||||
comment: "Title shown when no chargers are configured"
|
||||
)
|
||||
String(localized: "overview.chargers.empty.title", defaultValue: "No chargers configured yet")
|
||||
}
|
||||
|
||||
private var chargerEmptySubtitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.chargers.empty.subtitle",
|
||||
bundle: .main,
|
||||
value: "Add shore power, DC-DC, or solar chargers to understand your charging capacity.",
|
||||
comment: "Subtitle shown when no chargers are configured"
|
||||
)
|
||||
String(localized: "overview.chargers.empty.subtitle", defaultValue: "Add shore power, DC-DC, or solar chargers to understand your charging capacity.")
|
||||
}
|
||||
|
||||
private var chargerEmptyCreateAction: String {
|
||||
NSLocalizedString(
|
||||
"overview.chargers.empty.create",
|
||||
bundle: .main,
|
||||
value: "Add Charger",
|
||||
comment: "Button title to create a charger from the overview"
|
||||
)
|
||||
String(localized: "overview.chargers.empty.create", defaultValue: "Add Charger")
|
||||
}
|
||||
|
||||
private var systemOverviewTitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.system.header.title",
|
||||
bundle: .main,
|
||||
value: "System Overview",
|
||||
comment: "Title for system overview card"
|
||||
)
|
||||
String(localized: "overview.system.header.title", defaultValue: "System Overview")
|
||||
}
|
||||
|
||||
private var bomTitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.bom.title",
|
||||
bundle: .main,
|
||||
value: "Bill of Materials",
|
||||
comment: "Title for BOM metric card in the system overview"
|
||||
)
|
||||
String(localized: "overview.bom.title", defaultValue: "Bill of Materials")
|
||||
}
|
||||
|
||||
private var bomSubtitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.bom.subtitle",
|
||||
bundle: .main,
|
||||
value: "Tap to review components",
|
||||
comment: "Subtitle describing the BOM metric card interaction"
|
||||
)
|
||||
String(localized: "overview.bom.subtitle", defaultValue: "Tap to review components")
|
||||
}
|
||||
|
||||
private var bomPlaceholderSummary: String {
|
||||
NSLocalizedString(
|
||||
"overview.bom.placeholder.short",
|
||||
bundle: .main,
|
||||
value: "Add loads",
|
||||
comment: "Short placeholder shown when no BOM data is available"
|
||||
)
|
||||
String(localized: "overview.bom.placeholder.short", defaultValue: "Add loads")
|
||||
}
|
||||
|
||||
private var formattedBOMCompletedCount: String? {
|
||||
@@ -1235,30 +1090,15 @@ struct SystemOverviewView: View {
|
||||
}
|
||||
|
||||
private var chargeTimeTitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.chargetime.title",
|
||||
bundle: .main,
|
||||
value: "Estimated charge time",
|
||||
comment: "Title for the charge time metric card"
|
||||
)
|
||||
String(localized: "overview.chargetime.title", defaultValue: "Estimated charge time")
|
||||
}
|
||||
|
||||
private var chargeTimeSubtitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.chargetime.subtitle",
|
||||
bundle: .main,
|
||||
value: "At combined charge rate",
|
||||
comment: "Subtitle describing charge time assumptions"
|
||||
)
|
||||
String(localized: "overview.chargetime.subtitle", defaultValue: "At combined charge rate")
|
||||
}
|
||||
|
||||
private var chargeTimePlaceholderSummary: String {
|
||||
NSLocalizedString(
|
||||
"overview.chargetime.placeholder.short",
|
||||
bundle: .main,
|
||||
value: "Add chargers",
|
||||
comment: "Short placeholder shown when charge time cannot be calculated"
|
||||
)
|
||||
String(localized: "overview.chargetime.placeholder.short", defaultValue: "Add chargers")
|
||||
}
|
||||
|
||||
private var chargeGoalValueText: String? {
|
||||
@@ -1266,39 +1106,19 @@ struct SystemOverviewView: View {
|
||||
}
|
||||
|
||||
private var goalPrefix: String {
|
||||
NSLocalizedString(
|
||||
"overview.goal.prefix",
|
||||
bundle: .main,
|
||||
value: "Goal",
|
||||
comment: "Prefix displayed before goal value"
|
||||
)
|
||||
String(localized: "overview.goal.prefix", defaultValue: "Goal")
|
||||
}
|
||||
|
||||
private var runtimeTitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.runtime.title",
|
||||
bundle: .main,
|
||||
value: "Estimated runtime",
|
||||
comment: "Title for estimated runtime section"
|
||||
)
|
||||
String(localized: "overview.runtime.title", defaultValue: "Estimated runtime")
|
||||
}
|
||||
|
||||
private var runtimeSubtitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.runtime.subtitle",
|
||||
bundle: .main,
|
||||
value: "At current load draw",
|
||||
comment: "Subtitle describing runtime assumption"
|
||||
)
|
||||
String(localized: "overview.runtime.subtitle", defaultValue: "At maximum load draw")
|
||||
}
|
||||
|
||||
private var runtimePlaceholderSummary: String {
|
||||
NSLocalizedString(
|
||||
"overview.runtime.placeholder.short",
|
||||
bundle: .main,
|
||||
value: "Add capacity",
|
||||
comment: "Short placeholder shown when runtime cannot be calculated"
|
||||
)
|
||||
String(localized: "overview.runtime.placeholder.short", defaultValue: "Add capacity")
|
||||
}
|
||||
|
||||
private var runtimeGoalValueText: String? {
|
||||
@@ -1311,48 +1131,23 @@ struct SystemOverviewView: View {
|
||||
}
|
||||
|
||||
private var runtimeGoalSheetTitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.runtime.goal.title",
|
||||
bundle: .main,
|
||||
value: "Runtime Goal",
|
||||
comment: "Navigation title for editing the runtime goal"
|
||||
)
|
||||
String(localized: "overview.runtime.goal.title", defaultValue: "Runtime Goal")
|
||||
}
|
||||
|
||||
private var chargeGoalSheetTitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.chargetime.goal.title",
|
||||
bundle: .main,
|
||||
value: "Charge Goal",
|
||||
comment: "Navigation title for editing the charge time goal"
|
||||
)
|
||||
String(localized: "overview.chargetime.goal.title", defaultValue: "Charge Goal")
|
||||
}
|
||||
|
||||
private var goalClearTitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.goal.clear",
|
||||
bundle: .main,
|
||||
value: "Remove Goal",
|
||||
comment: "Button title to clear an active goal"
|
||||
)
|
||||
String(localized: "overview.goal.clear", defaultValue: "Remove Goal")
|
||||
}
|
||||
|
||||
private var goalCancelTitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.goal.cancel",
|
||||
bundle: .main,
|
||||
value: "Cancel",
|
||||
comment: "Button title to cancel goal editing"
|
||||
)
|
||||
String(localized: "overview.goal.cancel", defaultValue: "Cancel")
|
||||
}
|
||||
|
||||
private var goalSaveTitle: String {
|
||||
NSLocalizedString(
|
||||
"overview.goal.save",
|
||||
bundle: .main,
|
||||
value: "Save",
|
||||
comment: "Button title to save goal editing"
|
||||
)
|
||||
String(localized: "overview.goal.save", defaultValue: "Save")
|
||||
}
|
||||
|
||||
private static let numberFormatter: NumberFormatter = {
|
||||
@@ -1405,19 +1200,9 @@ struct SystemOverviewView: View {
|
||||
var shortLabel: String {
|
||||
switch self {
|
||||
case .voltage:
|
||||
return NSLocalizedString(
|
||||
"battery.bank.warning.voltage.short",
|
||||
bundle: .main,
|
||||
value: "Voltage",
|
||||
comment: "Short label for voltage warning"
|
||||
)
|
||||
return String(localized: "battery.bank.warning.voltage.short", defaultValue: "Voltage")
|
||||
case .capacity:
|
||||
return NSLocalizedString(
|
||||
"battery.bank.warning.capacity.short",
|
||||
bundle: .main,
|
||||
value: "Capacity",
|
||||
comment: "Short label for capacity warning"
|
||||
)
|
||||
return String(localized: "battery.bank.warning.capacity.short", defaultValue: "Capacity")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ struct SystemBillOfMaterialsPDFExporter {
|
||||
systemName: systemName,
|
||||
unitSystem: unitSystem
|
||||
)
|
||||
let emptyMessage = NSLocalizedString(
|
||||
"bom.pdf.placeholder.empty",
|
||||
comment: "Message shown in the PDF export when no components are available"
|
||||
let emptyMessage = String(
|
||||
localized: "bom.pdf.placeholder.empty",
|
||||
defaultValue: "No components available."
|
||||
)
|
||||
drawPlaceholder(in: context.cgContext, text: emptyMessage, at: cursorY)
|
||||
} else {
|
||||
@@ -89,17 +89,17 @@ struct SystemBillOfMaterialsPDFExporter {
|
||||
let titleFont = UIFont.systemFont(ofSize: isFirstPage ? 26 : 18, weight: .bold)
|
||||
let subtitleFont = UIFont.systemFont(ofSize: isFirstPage ? 16 : 12, weight: .medium)
|
||||
let title = isFirstPage
|
||||
? NSLocalizedString(
|
||||
"bom.pdf.header.title",
|
||||
comment: "Primary title shown at the top of the BOM PDF"
|
||||
? String(
|
||||
localized: "bom.pdf.header.title",
|
||||
defaultValue: "System Bill of Materials"
|
||||
)
|
||||
: systemName
|
||||
|
||||
let subtitle: String
|
||||
if isFirstPage {
|
||||
let format = NSLocalizedString(
|
||||
"bom.pdf.header.subtitle",
|
||||
comment: "Subtitle format combining system name and unit system for the BOM PDF"
|
||||
let format = String(
|
||||
localized: "bom.pdf.header.subtitle",
|
||||
defaultValue: "%@ • %@"
|
||||
)
|
||||
subtitle = String(
|
||||
format: format,
|
||||
@@ -108,9 +108,9 @@ struct SystemBillOfMaterialsPDFExporter {
|
||||
unitSystem.displayName
|
||||
)
|
||||
} else {
|
||||
let format = NSLocalizedString(
|
||||
"bom.pdf.header.inline",
|
||||
comment: "Subtitle describing the active unit system on subsequent PDF pages"
|
||||
let format = String(
|
||||
localized: "bom.pdf.header.inline",
|
||||
defaultValue: "Unit System: %@"
|
||||
)
|
||||
subtitle = String(
|
||||
format: format,
|
||||
@@ -286,9 +286,9 @@ struct SystemBillOfMaterialsPDFExporter {
|
||||
.font: footerFont,
|
||||
.foregroundColor: tertiaryTextColor
|
||||
]
|
||||
let format = NSLocalizedString(
|
||||
"bom.pdf.page.number",
|
||||
comment: "Format string for the PDF page number footer"
|
||||
let format = String(
|
||||
localized: "bom.pdf.page.number",
|
||||
defaultValue: "Page %d"
|
||||
)
|
||||
let text = String(format: format, locale: Locale.current, pageIndex)
|
||||
let size = text.size(withAttributes: attributes)
|
||||
|
||||
@@ -40,60 +40,30 @@ struct SystemBillOfMaterialsView: View {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .components:
|
||||
return NSLocalizedString(
|
||||
"bom.category.components.title",
|
||||
comment: "Section title for core components and chargers"
|
||||
)
|
||||
return String(localized: "bom.category.components.title", defaultValue: "Components & Chargers")
|
||||
case .batteries:
|
||||
return NSLocalizedString(
|
||||
"bom.category.batteries.title",
|
||||
comment: "Section title for batteries"
|
||||
)
|
||||
return String(localized: "bom.category.batteries.title", defaultValue: "Batteries")
|
||||
case .cables:
|
||||
return NSLocalizedString(
|
||||
"bom.category.cables.title",
|
||||
comment: "Section title for power cables"
|
||||
)
|
||||
return String(localized: "bom.category.cables.title", defaultValue: "Cables")
|
||||
case .fuses:
|
||||
return NSLocalizedString(
|
||||
"bom.category.fuses.title",
|
||||
comment: "Section title for fuses and holders"
|
||||
)
|
||||
return String(localized: "bom.category.fuses.title", defaultValue: "Fuses")
|
||||
case .accessories:
|
||||
return NSLocalizedString(
|
||||
"bom.category.accessories.title",
|
||||
comment: "Section title for accessory hardware"
|
||||
)
|
||||
return String(localized: "bom.category.accessories.title", defaultValue: "Accessories")
|
||||
}
|
||||
}
|
||||
|
||||
var subtitle: String {
|
||||
switch self {
|
||||
case .components:
|
||||
return NSLocalizedString(
|
||||
"bom.category.components.subtitle",
|
||||
comment: "Subtitle describing the components section"
|
||||
)
|
||||
return String(localized: "bom.category.components.subtitle", defaultValue: "Primary devices, controllers, and charging gear.")
|
||||
case .batteries:
|
||||
return NSLocalizedString(
|
||||
"bom.category.batteries.subtitle",
|
||||
comment: "Subtitle describing the batteries section"
|
||||
)
|
||||
return String(localized: "bom.category.batteries.subtitle", defaultValue: "House banks and storage.")
|
||||
case .cables:
|
||||
return NSLocalizedString(
|
||||
"bom.category.cables.subtitle",
|
||||
comment: "Subtitle describing the cables section"
|
||||
)
|
||||
return String(localized: "bom.category.cables.subtitle", defaultValue: "Sized power runs for every circuit.")
|
||||
case .fuses:
|
||||
return NSLocalizedString(
|
||||
"bom.category.fuses.subtitle",
|
||||
comment: "Subtitle describing the fuses section"
|
||||
)
|
||||
return String(localized: "bom.category.fuses.subtitle", defaultValue: "Circuit protection and holders.")
|
||||
case .accessories:
|
||||
return NSLocalizedString(
|
||||
"bom.category.accessories.subtitle",
|
||||
comment: "Subtitle describing the accessories section"
|
||||
)
|
||||
return String(localized: "bom.category.accessories.subtitle", defaultValue: "Fuses, lugs, and supporting hardware.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,44 +135,26 @@ struct SystemBillOfMaterialsView: View {
|
||||
if let scale = quantityScale {
|
||||
guard let unit = quantifiedDetailContext else { return nil }
|
||||
let value = Double(quantity) / scale
|
||||
let format = NSLocalizedString(
|
||||
"bom.quantity.cable.badge",
|
||||
comment: "Metric text for total cable length including cross section"
|
||||
)
|
||||
let format = String(localized: "bom.quantity.cable.badge", defaultValue: "%1$.1f %2$@ · %3$@")
|
||||
return String(format: format, locale: Locale.current, value, unit, quantifiedDetailSecondaryContext ?? "")
|
||||
} else if quantity > 1 {
|
||||
if let fuseRating = quantifiedDetailSecondaryContext, let amps = Int(fuseRating) {
|
||||
let format = NSLocalizedString(
|
||||
"bom.quantity.fuse.badge",
|
||||
comment: "Metric text for consolidated fuses"
|
||||
)
|
||||
let format = String(localized: "bom.quantity.fuse.badge", defaultValue: "%1$d× · %2$d A")
|
||||
return String(format: format, quantity, amps)
|
||||
}
|
||||
if let gauge = quantifiedDetailContext {
|
||||
let format = NSLocalizedString(
|
||||
"bom.quantity.terminal.badge",
|
||||
comment: "Metric text for consolidated terminals"
|
||||
)
|
||||
let format = String(localized: "bom.quantity.terminal.badge", defaultValue: "%1$d× · %2$@")
|
||||
return String(format: format, quantity, gauge)
|
||||
}
|
||||
let format = NSLocalizedString(
|
||||
"bom.quantity.count.badge",
|
||||
comment: "Metric text for counted items"
|
||||
)
|
||||
let format = String(localized: "bom.quantity.count.badge", defaultValue: "%d×")
|
||||
return String(format: format, quantity)
|
||||
}
|
||||
if let fuseRating = quantifiedDetailSecondaryContext, let amps = Int(fuseRating) {
|
||||
let format = NSLocalizedString(
|
||||
"bom.quantity.fuse.badge",
|
||||
comment: "Metric text for consolidated fuses"
|
||||
)
|
||||
let format = String(localized: "bom.quantity.fuse.badge", defaultValue: "%1$d× · %2$d A")
|
||||
return String(format: format, 1, amps)
|
||||
}
|
||||
if let gauge = quantifiedDetailContext, !gauge.isEmpty {
|
||||
let format = NSLocalizedString(
|
||||
"bom.quantity.single.badge",
|
||||
comment: "Metric text when quantity is one but should be explicit"
|
||||
)
|
||||
let format = String(localized: "bom.quantity.single.badge", defaultValue: "1× • %@")
|
||||
return String(format: format, gauge)
|
||||
}
|
||||
return nil
|
||||
@@ -313,10 +265,7 @@ struct SystemBillOfMaterialsView: View {
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle(
|
||||
String(
|
||||
format: NSLocalizedString(
|
||||
"bom.navigation.title.system",
|
||||
comment: "Navigation title for the bill of materials view"
|
||||
),
|
||||
format: String(localized: "bom.navigation.title.system", defaultValue: "BOM – %@"),
|
||||
locale: Locale.current,
|
||||
systemName
|
||||
)
|
||||
@@ -337,10 +286,7 @@ struct SystemBillOfMaterialsView: View {
|
||||
.progressViewStyle(.circular)
|
||||
} else {
|
||||
Label(
|
||||
NSLocalizedString(
|
||||
"bom.export.pdf.button",
|
||||
comment: "Button title for exporting the BOM as a PDF document"
|
||||
),
|
||||
String(localized: "bom.export.pdf.button", defaultValue: "Export PDF"),
|
||||
systemImage: "square.and.arrow.up"
|
||||
)
|
||||
}
|
||||
@@ -361,18 +307,12 @@ struct SystemBillOfMaterialsView: View {
|
||||
.alert(item: $exportError) { error in
|
||||
Alert(
|
||||
title: Text(
|
||||
NSLocalizedString(
|
||||
"bom.export.pdf.error.title",
|
||||
comment: "Title for the alert shown when a PDF export fails"
|
||||
)
|
||||
String(localized: "bom.export.pdf.error.title", defaultValue: "Export Failed")
|
||||
),
|
||||
message: Text(error.message),
|
||||
dismissButton: .default(
|
||||
Text(
|
||||
NSLocalizedString(
|
||||
"generic.ok",
|
||||
comment: "Default acknowledgement button title"
|
||||
)
|
||||
String(localized: "generic.ok", defaultValue: "OK")
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -382,10 +322,7 @@ struct SystemBillOfMaterialsView: View {
|
||||
private func exportBillOfMaterialsPDF() {
|
||||
if categorySections.isEmpty {
|
||||
exportError = ExportError(
|
||||
message: NSLocalizedString(
|
||||
"bom.export.pdf.error.empty",
|
||||
comment: "Error message shown when attempting to export a PDF without any components"
|
||||
)
|
||||
message: String(localized: "bom.export.pdf.error.empty", defaultValue: "Add at least one component before exporting.")
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -436,11 +373,9 @@ struct SystemBillOfMaterialsView: View {
|
||||
|
||||
HStack(spacing: 12) {
|
||||
let accessibilityLabel: String = {
|
||||
let formatKey = isCompleted ? "bom.accessibility.mark.incomplete" : "bom.accessibility.mark.complete"
|
||||
let format = NSLocalizedString(
|
||||
formatKey,
|
||||
comment: "Accessibility label instructing VoiceOver to toggle a BOM item"
|
||||
)
|
||||
let format = isCompleted
|
||||
? String(localized: "bom.accessibility.mark.incomplete", defaultValue: "Mark %@ incomplete")
|
||||
: String(localized: "bom.accessibility.mark.complete", defaultValue: "Mark %@ complete")
|
||||
return String.localizedStringWithFormat(format, item.title)
|
||||
}()
|
||||
|
||||
@@ -620,9 +555,9 @@ struct SystemBillOfMaterialsView: View {
|
||||
|
||||
if unitSystem == .imperial {
|
||||
let awg = awgFromCrossSection(load.crossSection)
|
||||
if awg > 0 {
|
||||
crossSectionLabel = String(format: "AWG %.0f", awg)
|
||||
gaugeQuery = String(format: "AWG %.0f", awg)
|
||||
if load.crossSection > 0 {
|
||||
crossSectionLabel = "AWG \(ElectricalCalculations.formatAWG(awg))"
|
||||
gaugeQuery = "AWG \(ElectricalCalculations.formatAWG(awg))"
|
||||
} else {
|
||||
crossSectionLabel = unknownSizeLabel
|
||||
gaugeQuery = "battery cable"
|
||||
@@ -641,34 +576,28 @@ struct SystemBillOfMaterialsView: View {
|
||||
let powerDetail = String(format: "%.0f W @ %.1f V", calculatedPower, load.voltage)
|
||||
|
||||
let fuseRating = recommendedFuse(for: load)
|
||||
let fuseDetailFormat = NSLocalizedString(
|
||||
"bom.fuse.detail",
|
||||
comment: "Description for the fuse item in the BOM list"
|
||||
)
|
||||
let fuseDetailFormat = String(localized: "bom.fuse.detail", defaultValue: "Inline holder and %dA fuse")
|
||||
let fuseDetail = String.localizedStringWithFormat(fuseDetailFormat, fuseRating)
|
||||
|
||||
let terminalCount = 4
|
||||
let cableShoesDetailFormat = NSLocalizedString(
|
||||
"bom.terminals.detail",
|
||||
comment: "Description for the cable terminals item in the BOM list"
|
||||
)
|
||||
let cableShoesDetailFormat = String(localized: "bom.terminals.detail", defaultValue: "Ring or spade terminals sized for %@ wiring")
|
||||
let cableShoesDetail = String.localizedStringWithFormat(
|
||||
cableShoesDetailFormat,
|
||||
crossSectionLabel.lowercased()
|
||||
)
|
||||
let affiliateURL = load.affiliateURLString.flatMap { URL(string: $0) }
|
||||
let deviceFallbackFormat = NSLocalizedString("bom.search.device.fallback", comment: "Amazon search query fallback for a DC device")
|
||||
let deviceFallbackFormat = String(localized: "bom.search.device.fallback", defaultValue: "DC device %.0fW %.0fV")
|
||||
let deviceQuery = load.name.isEmpty
|
||||
? String(format: deviceFallbackFormat, calculatedPower, load.voltage)
|
||||
: load.name
|
||||
|
||||
let redCableSearchFormat = NSLocalizedString("bom.search.cable.red", comment: "Amazon search query for red battery cable")
|
||||
let redCableSearchFormat = String(localized: "bom.search.cable.red", defaultValue: "%@ red battery cable")
|
||||
let redCableQuery = String(format: redCableSearchFormat, gaugeQuery)
|
||||
let blackCableSearchFormat = NSLocalizedString("bom.search.cable.black", comment: "Amazon search query for black battery cable")
|
||||
let blackCableSearchFormat = String(localized: "bom.search.cable.black", defaultValue: "%@ black battery cable")
|
||||
let blackCableQuery = String(format: blackCableSearchFormat, gaugeQuery)
|
||||
let fuseSearchFormat = NSLocalizedString("bom.search.fuse", comment: "Amazon search query for inline fuse holder")
|
||||
let fuseSearchFormat = String(localized: "bom.search.fuse", defaultValue: "inline fuse holder %dA")
|
||||
let fuseQuery = String(format: fuseSearchFormat, fuseRating)
|
||||
let terminalSearchFormat = NSLocalizedString("bom.search.terminals", comment: "Amazon search query for cable shoes")
|
||||
let terminalSearchFormat = String(localized: "bom.search.terminals", defaultValue: "%@ cable shoes")
|
||||
let terminalQuery = String(format: terminalSearchFormat, gaugeQuery)
|
||||
|
||||
let componentStorageKey = Self.storageKey(for: component, itemID: "component")
|
||||
@@ -775,19 +704,13 @@ struct SystemBillOfMaterialsView: View {
|
||||
|
||||
private func batteryItems(for battery: SavedBattery) -> [Item] {
|
||||
let component: Component = .battery(battery)
|
||||
let usableCapacityLabel = NSLocalizedString(
|
||||
"battery.bank.metric.usable_capacity",
|
||||
comment: "Label describing usable battery capacity"
|
||||
)
|
||||
let usableCapacityLabel = String(localized: "battery.bank.metric.usable_capacity", defaultValue: "Usable Capacity")
|
||||
let capacityLabel = String(format: "%.0f Ah @ %.1f V", battery.capacityAmpHours, battery.nominalVoltage)
|
||||
let usableLabel = String(format: "%@: %.0f Ah", usableCapacityLabel, battery.usableCapacityAmpHours)
|
||||
let detail = [capacityLabel, battery.chemistry.displayName, usableLabel].joined(separator: " • ")
|
||||
let capacityQuery = max(1, Int(round(battery.capacityAmpHours)))
|
||||
let voltageQuery = max(1, Int(round(battery.nominalVoltage)))
|
||||
let batterySearchFormat = NSLocalizedString(
|
||||
"bom.search.battery",
|
||||
comment: "Amazon search query for a battery"
|
||||
)
|
||||
let batterySearchFormat = String(localized: "bom.search.battery", defaultValue: "%dAh %dV %@ battery")
|
||||
let query = String(format: batterySearchFormat, capacityQuery, voltageQuery, battery.chemistry.displayName)
|
||||
let affiliateURL = battery.affiliateURLString.flatMap { URL(string: $0) }
|
||||
let storageKey = Self.storageKey(for: component, itemID: "battery")
|
||||
@@ -933,15 +856,16 @@ struct SystemBillOfMaterialsView: View {
|
||||
completedItemIDs = Set(loadKeys + batteryKeys + chargerKeys)
|
||||
}
|
||||
|
||||
private func recommendedFuse(for load: SavedLoad) -> Int {
|
||||
ElectricalCalculations.recommendedFuse(forCurrent: load.current)
|
||||
private func recommendedFuse(for load: SavedLoad) -> String {
|
||||
let fuse = ElectricalCalculations.recommendedFuse(forCurrent: load.current)
|
||||
return fuse == fuse.rounded() ? String(format: "%.0f", fuse) : String(format: "%.1f", fuse)
|
||||
}
|
||||
|
||||
private func awgFromCrossSection(_ crossSectionMM2: Double) -> Double {
|
||||
let mapping: [(awg: Double, area: Double)] = [
|
||||
(20, 0.519), (18, 0.823), (16, 1.31), (14, 2.08), (12, 3.31), (10, 5.26),
|
||||
(8, 8.37), (6, 13.3), (4, 21.2), (2, 33.6), (1, 42.4), (0, 53.5),
|
||||
(00, 67.4), (000, 85.0), (0000, 107.0)
|
||||
(8, 8.37), (6, 13.3), (4, 21.2), (2, 33.6), (1, 42.4), (-1, 53.5),
|
||||
(-2, 67.4), (-3, 85.0), (-4, 107.0)
|
||||
]
|
||||
|
||||
guard crossSectionMM2 > 0 else { return 0 }
|
||||
@@ -954,10 +878,7 @@ struct SystemBillOfMaterialsView: View {
|
||||
}
|
||||
|
||||
private var footerMessage: String {
|
||||
NSLocalizedString(
|
||||
"affiliate.disclaimer",
|
||||
comment: "Footer note reminding users that affiliate purchases may support the app"
|
||||
)
|
||||
String(localized: "affiliate.disclaimer", defaultValue: "Purchases through affiliate links may support VoltPlan.")
|
||||
}
|
||||
|
||||
private var dateFormatter: DateFormatter {
|
||||
|
||||
@@ -98,11 +98,9 @@ struct SystemComponentsPersistence {
|
||||
existingBatteries: [SavedBattery],
|
||||
existingChargers: [SavedCharger]
|
||||
) -> BatteryConfiguration {
|
||||
let defaultName = NSLocalizedString(
|
||||
"battery.editor.default_name",
|
||||
bundle: .main,
|
||||
value: "New Battery",
|
||||
comment: "Default name when configuring a new battery"
|
||||
let defaultName = String(
|
||||
localized: "battery.editor.default_name",
|
||||
defaultValue: "New Battery"
|
||||
)
|
||||
let batteryName = uniqueName(
|
||||
startingWith: defaultName,
|
||||
@@ -124,11 +122,9 @@ struct SystemComponentsPersistence {
|
||||
existingBatteries: [SavedBattery],
|
||||
existingChargers: [SavedCharger]
|
||||
) -> ChargerConfiguration {
|
||||
let defaultName = NSLocalizedString(
|
||||
"charger.editor.default_name",
|
||||
bundle: .main,
|
||||
value: "New Charger",
|
||||
comment: "Default name when configuring a new charger"
|
||||
let defaultName = String(
|
||||
localized: "charger.editor.default_name",
|
||||
defaultValue: "New Charger"
|
||||
)
|
||||
let chargerName = uniqueName(
|
||||
startingWith: defaultName,
|
||||
|
||||
@@ -472,10 +472,7 @@ struct SystemsView: View {
|
||||
formattedPower = String(format: "%.0fW", totalPower)
|
||||
}
|
||||
|
||||
let format = NSLocalizedString(
|
||||
"system.list.component.summary",
|
||||
comment: "Summary showing number of components and the total power"
|
||||
)
|
||||
let format = String(localized: "system.list.component.summary", defaultValue: "%#@component_count@ • %@")
|
||||
return String.localizedStringWithFormat(format, count, formattedPower)
|
||||
}
|
||||
|
||||
@@ -499,12 +496,10 @@ struct SystemsView: View {
|
||||
|
||||
private func keywords(for localizationKey: String, fallback: [String]) -> [String] {
|
||||
let fallbackValue = fallback.joined(separator: ",")
|
||||
let localizedKeywords = NSLocalizedString(
|
||||
localizationKey,
|
||||
tableName: nil,
|
||||
bundle: .main,
|
||||
let localizedKeywords = Bundle.main.localizedString(
|
||||
forKey: localizationKey,
|
||||
value: fallbackValue,
|
||||
comment: ""
|
||||
table: nil
|
||||
)
|
||||
let separators = CharacterSet(charactersIn: ",;")
|
||||
let components = localizedKeywords
|
||||
|
||||
@@ -207,7 +207,8 @@ extension UITestSampleData {
|
||||
iconName: "bolt.badge.clock",
|
||||
colorName: "blue",
|
||||
system: adventureVan,
|
||||
identifier: "sample.charger.dcdc"
|
||||
identifier: "sample.charger.dcdc",
|
||||
powerSourceType: "alternator"
|
||||
)
|
||||
alternatorCharger.timestamp = Date(timeIntervalSinceReferenceDate: 1350)
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"battery.bank.status.voltage.title" = "Spannungsabweichung";
|
||||
"battery.bank.warning.capacity.short" = "Kapazität";
|
||||
"battery.bank.warning.voltage.short" = "Spannung";
|
||||
"battery.editor.advanced.usable_capacity.footer_default" = "Standardwert %@ basierend auf der Chemie.";
|
||||
"battery.editor.advanced.usable_capacity.footer_default" = "Standard %@ basierend auf der Chemie.";
|
||||
"battery.editor.advanced.usable_capacity.footer_override" = "Überschreibung aktiv. Chemie-Standard bleibt %@.";
|
||||
"battery.editor.advanced.charge_voltage.helper" = "Lege die maximal empfohlene Ladespannung fest.";
|
||||
"battery.editor.advanced.cutoff_voltage.helper" = "Lege die minimale sichere Entladespannung fest.";
|
||||
@@ -442,6 +442,7 @@
|
||||
"charger.source.solar" = "Solar";
|
||||
"charger.source.wind" = "Wind";
|
||||
"charger.source.generator" = "Generator";
|
||||
"charger.source.alternator" = "Lichtmaschine";
|
||||
|
||||
// MARK: - Share Menu
|
||||
"overview.share.diagram" = "Schaltplan";
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
"overview.loads.empty.title" = "Aún no hay cargas configuradas";
|
||||
"overview.loads.empty.subtitle" = "Añade cargas para obtener recomendaciones de cables y fusibles adaptadas a este sistema.";
|
||||
"overview.runtime.title" = "Autonomía estimada";
|
||||
"overview.runtime.subtitle" = "Con la carga actual";
|
||||
"overview.runtime.subtitle" = "Con la carga máxima";
|
||||
"overview.runtime.unavailable" = "Añade capacidad de batería y potencia de carga para estimar la autonomía.";
|
||||
"overview.bom.title" = "Lista de materiales";
|
||||
"overview.bom.subtitle" = "Pulsa para revisar los componentes";
|
||||
@@ -273,7 +273,7 @@
|
||||
"battery.editor.slider.temperature_range.max" = "Máximo";
|
||||
"battery.editor.section.advanced" = "Avanzado";
|
||||
"battery.editor.button.reset_default" = "Restablecer";
|
||||
"battery.editor.advanced.usable_capacity.footer_default" = "Valor predeterminado %@ basado en la química.";
|
||||
"battery.editor.advanced.usable_capacity.footer_default" = "Predeterminado %@ según la química.";
|
||||
"battery.editor.advanced.usable_capacity.footer_override" = "Sobrescritura activa. El valor predeterminado por química sigue siendo %@.";
|
||||
"battery.editor.advanced.charge_voltage.helper" = "Establece el voltaje máximo de carga recomendado.";
|
||||
"battery.editor.advanced.cutoff_voltage.helper" = "Establece el voltaje mínimo seguro de descarga.";
|
||||
@@ -404,6 +404,7 @@
|
||||
"charger.source.solar" = "Solar";
|
||||
"charger.source.wind" = "Eólica";
|
||||
"charger.source.generator" = "Generador";
|
||||
"charger.source.alternator" = "Alternador";
|
||||
|
||||
// MARK: - Share Menu
|
||||
"overview.share.diagram" = "Diagrama de cableado";
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
"overview.loads.empty.title" = "Aucune charge configurée pour l'instant";
|
||||
"overview.loads.empty.subtitle" = "Ajoutez des charges pour obtenir des recommandations de câbles et de fusibles adaptées à ce système.";
|
||||
"overview.runtime.title" = "Autonomie estimée";
|
||||
"overview.runtime.subtitle" = "Avec la charge actuelle";
|
||||
"overview.runtime.subtitle" = "À charge maximale";
|
||||
"overview.runtime.unavailable" = "Ajoutez la capacité de la batterie et la puissance consommée pour estimer l’autonomie.";
|
||||
"overview.bom.title" = "Liste de matériel";
|
||||
"overview.bom.subtitle" = "Touchez pour consulter les composants";
|
||||
@@ -273,7 +273,7 @@
|
||||
"battery.editor.slider.temperature_range.max" = "Maximum";
|
||||
"battery.editor.section.advanced" = "Avancé";
|
||||
"battery.editor.button.reset_default" = "Réinitialiser";
|
||||
"battery.editor.advanced.usable_capacity.footer_default" = "Valeur par défaut %@ selon la chimie.";
|
||||
"battery.editor.advanced.usable_capacity.footer_default" = "Par défaut %@ selon la chimie.";
|
||||
"battery.editor.advanced.usable_capacity.footer_override" = "Remplacement actif. La valeur par défaut liée à la chimie reste %@.";
|
||||
"battery.editor.advanced.charge_voltage.helper" = "Définissez la tension de charge maximale recommandée.";
|
||||
"battery.editor.advanced.cutoff_voltage.helper" = "Définissez la tension minimale de décharge sûre.";
|
||||
@@ -404,6 +404,7 @@
|
||||
"charger.source.solar" = "Solaire";
|
||||
"charger.source.wind" = "Éolienne";
|
||||
"charger.source.generator" = "Groupe électrogène";
|
||||
"charger.source.alternator" = "Alternateur";
|
||||
|
||||
// MARK: - Share Menu
|
||||
"overview.share.diagram" = "Schéma de câblage";
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
"overview.loads.empty.title" = "Nog geen lasten geconfigureerd";
|
||||
"overview.loads.empty.subtitle" = "Voeg lasten toe om kabel- en zekeringsadviezen te krijgen die zijn afgestemd op dit systeem.";
|
||||
"overview.runtime.title" = "Geschatte looptijd";
|
||||
"overview.runtime.subtitle" = "Bij huidige belasting";
|
||||
"overview.runtime.subtitle" = "Bij maximale belasting";
|
||||
"overview.runtime.unavailable" = "Voeg batterijcapaciteit en vermogensvraag toe om de looptijd te berekenen.";
|
||||
"overview.bom.title" = "Stuklijst";
|
||||
"overview.bom.subtitle" = "Tik om componenten te bekijken";
|
||||
@@ -273,8 +273,8 @@
|
||||
"battery.editor.slider.temperature_range.max" = "Maximum";
|
||||
"battery.editor.section.advanced" = "Geavanceerd";
|
||||
"battery.editor.button.reset_default" = "Resetten";
|
||||
"battery.editor.advanced.usable_capacity.footer_default" = "Standaardwaarde %@ op basis van de chemie.";
|
||||
"battery.editor.advanced.usable_capacity.footer_override" = "Handmatige override actief. Chemische standaard blijft %@.";
|
||||
"battery.editor.advanced.usable_capacity.footer_default" = "Standaard %@ op basis van de chemie.";
|
||||
"battery.editor.advanced.usable_capacity.footer_override" = "Override actief. Chemische standaard blijft %@.";
|
||||
"battery.editor.advanced.charge_voltage.helper" = "Stel de maximaal aanbevolen laadspanning in.";
|
||||
"battery.editor.advanced.cutoff_voltage.helper" = "Stel de minimale veilige ontlaadspanning in.";
|
||||
"battery.editor.advanced.temperature_range.helper" = "Bepaal het aanbevolen temperatuurbereik voor gebruik.";
|
||||
@@ -404,6 +404,7 @@
|
||||
"charger.source.solar" = "Zonne-energie";
|
||||
"charger.source.wind" = "Wind";
|
||||
"charger.source.generator" = "Generator";
|
||||
"charger.source.alternator" = "Dynamo";
|
||||
|
||||
// MARK: - Share Menu
|
||||
"overview.share.diagram" = "Bedradingsschema";
|
||||
|
||||
Reference in New Issue
Block a user