Compare commits

...

4 Commits

Author SHA1 Message Date
Stefan Lange-Hegermann
10dc0e4fa9 translated missing elements 2025-10-28 23:23:40 +01:00
Stefan Lange-Hegermann
8868368392 better layout of the advanced sections 2025-10-28 22:53:37 +01:00
Stefan Lange-Hegermann
a2314585ea added subscription booking 2025-10-28 22:43:41 +01:00
Stefan Lange-Hegermann
46664625b4 some advanced settings 2025-10-28 13:31:51 +01:00
16 changed files with 3071 additions and 696 deletions

View File

@@ -2,6 +2,77 @@
"affiliate.description.with_link" = "Tapping above shows a full bill of materials before opening the affiliate link. Purchases may support VoltPlan.";
"affiliate.description.without_link" = "Tapping above shows a full bill of materials with shopping searches to help you source parts.";
"affiliate.disclaimer" = "Purchases through affiliate links may support VoltPlan.";
"battery.bank.badge.capacity" = "Capacity";
"battery.bank.badge.energy" = "Energy";
"battery.bank.badge.voltage" = "Voltage";
"battery.bank.banner.capacity" = "Capacity mismatch detected";
"battery.bank.banner.voltage" = "Voltage mismatch detected";
"battery.bank.empty.subtitle" = "Tap the plus button to configure a battery for %@.";
"battery.bank.empty.title" = "No Batteries Yet";
"battery.bank.header.title" = "Battery Bank";
"battery.bank.metric.capacity" = "Capacity";
"battery.bank.metric.count" = "Batteries";
"battery.bank.metric.energy" = "Energy";
"battery.bank.metric.usable_capacity" = "Usable Capacity";
"battery.bank.metric.usable_energy" = "Usable Energy";
"battery.bank.status.capacity.message" = "%@ uses a different capacity than the dominant bank size of %@. Mismatched capacities cause uneven discharge and premature wear.";
"battery.bank.status.capacity.title" = "Capacity mismatch";
"battery.bank.status.dismiss" = "Got it";
"battery.bank.status.multiple.batteries" = "%d batteries";
"battery.bank.status.single.battery" = "One battery";
"battery.bank.status.voltage.message" = "%@ diverges from the bank baseline of %@. Mixing nominal voltages leads to uneven charging and can damage connected chargers or inverters.";
"battery.bank.status.voltage.title" = "Voltage mismatch";
"battery.bank.warning.capacity.short" = "Capacity";
"battery.bank.warning.voltage.short" = "Voltage";
"battery.editor.advanced.usable_capacity.footer_default" = "Defaults to %@ based on chemistry.";
"battery.editor.advanced.usable_capacity.footer_override" = "Override active. Chemistry default remains %@.";
"battery.editor.advanced.charge_voltage.helper" = "Set the maximum recommended charging voltage.";
"battery.editor.advanced.cutoff_voltage.helper" = "Set the minimum safe discharge voltage.";
"battery.editor.advanced.temperature_range.helper" = "Define the recommended operating temperature range.";
"battery.editor.alert.charge_voltage.message" = "Enter charge voltage in volts (V)";
"battery.editor.alert.charge_voltage.placeholder" = "Charge Voltage";
"battery.editor.alert.charge_voltage.title" = "Edit Charge Voltage";
"battery.editor.alert.cutoff_voltage.message" = "Enter cut-off voltage in volts (V)";
"battery.editor.alert.cutoff_voltage.placeholder" = "Cut-off Voltage";
"battery.editor.alert.cutoff_voltage.title" = "Edit Cut-off Voltage";
"battery.editor.alert.maximum_temperature.message" = "Enter maximum temperature in degrees Celsius (\u00B0C)";
"battery.editor.alert.maximum_temperature.placeholder" = "Maximum Temperature (\u00B0C)";
"battery.editor.alert.maximum_temperature.title" = "Edit Maximum Temperature";
"battery.editor.alert.minimum_temperature.message" = "Enter minimum temperature in degrees Celsius (\u00B0C)";
"battery.editor.alert.minimum_temperature.placeholder" = "Minimum Temperature (\u00B0C)";
"battery.editor.alert.minimum_temperature.title" = "Edit Minimum Temperature";
"battery.editor.alert.cancel" = "Cancel";
"battery.editor.alert.capacity.message" = "Enter capacity in amp-hours (Ah)";
"battery.editor.alert.capacity.placeholder" = "Capacity";
"battery.editor.alert.capacity.title" = "Edit Capacity";
"battery.editor.alert.save" = "Save";
"battery.editor.alert.usable_capacity.message" = "Enter usable capacity percentage (%)";
"battery.editor.alert.usable_capacity.placeholder" = "Usable Capacity (%)";
"battery.editor.alert.usable_capacity.title" = "Edit Usable Capacity";
"battery.editor.alert.voltage.message" = "Enter voltage in volts (V)";
"battery.editor.alert.voltage.placeholder" = "Voltage";
"battery.editor.alert.voltage.title" = "Edit Nominal Voltage";
"battery.editor.button.reset_default" = "Reset";
"battery.editor.cancel" = "Cancel";
"battery.editor.default_name" = "New Battery";
"battery.editor.field.chemistry" = "Chemistry";
"battery.editor.field.name" = "Name";
"battery.editor.placeholder.name" = "House Bank";
"battery.editor.save" = "Save";
"battery.editor.section.advanced" = "Advanced";
"battery.editor.section.summary" = "Summary";
"battery.editor.slider.capacity" = "Capacity";
"battery.editor.slider.charge_voltage" = "Charge Voltage";
"battery.editor.slider.cutoff_voltage" = "Cut-off Voltage";
"battery.editor.slider.temperature_range" = "Temperature Range";
"battery.editor.slider.temperature_range.max" = "Maximum";
"battery.editor.slider.temperature_range.min" = "Minimum";
"battery.editor.slider.usable_capacity" = "Usable Capacity (%)";
"battery.editor.slider.voltage" = "Nominal Voltage";
"battery.editor.title" = "Battery Setup";
"battery.onboarding.subtitle" = "Track your bank's capacity and chemistry to keep runtime expectations in check.";
"battery.onboarding.title" = "Add your first battery";
"battery.overview.empty.create" = "Add Battery";
"bom.accessibility.mark.complete" = "Mark %@ complete";
"bom.accessibility.mark.incomplete" = "Mark %@ incomplete";
"bom.fuse.detail" = "Inline holder and %dA fuse";
@@ -13,11 +84,66 @@
"bom.navigation.title.system" = "BOM %@";
"bom.size.unknown" = "Size TBD";
"bom.terminals.detail" = "Ring or spade terminals sized for %@ wiring";
"cable.pro.privacy.label" = "Privacy";
"cable.pro.privacy.url" = "https://voltplan.app/privacy";
"cable.pro.terms.label" = "Terms";
"cable.pro.terms.url" = "https://voltplan.app/terms";
"calculator.advanced.duty_cycle.helper" = "Percentage of each active session where the load actually draws power.";
"calculator.advanced.duty_cycle.title" = "Duty Cycle";
"calculator.advanced.section.title" = "Advanced Settings";
"calculator.advanced.usage_hours.helper" = "Hours per day the load is turned on.";
"calculator.advanced.usage_hours.title" = "Daily On-Time";
"calculator.advanced.usage_hours.unit" = "h/day";
"calculator.alert.duty_cycle.message" = "Enter duty cycle as a percentage (0-100%).";
"calculator.alert.duty_cycle.placeholder" = "Duty Cycle";
"calculator.alert.duty_cycle.title" = "Edit Duty Cycle";
"calculator.alert.usage_hours.message" = "Enter the number of hours per day the load is active.";
"calculator.alert.usage_hours.placeholder" = "Daily On-Time";
"calculator.alert.usage_hours.title" = "Edit Daily On-Time";
"charger.default.new" = "New Charger";
"charger.editor.alert.cancel" = "Cancel";
"charger.editor.alert.current.message" = "Enter current in amps (A)";
"charger.editor.alert.current.title" = "Edit Charge Current";
"charger.editor.alert.input_voltage.title" = "Edit Input Voltage";
"charger.editor.alert.output_voltage.title" = "Edit Output Voltage";
"charger.editor.alert.power.message" = "Enter power in watts (W)";
"charger.editor.alert.power.placeholder" = "Power";
"charger.editor.alert.power.title" = "Edit Charge Power";
"charger.editor.alert.save" = "Save";
"charger.editor.alert.voltage.message" = "Enter voltage in volts (V)";
"charger.editor.appearance.accessibility" = "Edit charger appearance";
"charger.editor.appearance.subtitle" = "Customize how this charger shows up";
"charger.editor.appearance.title" = "Charger Appearance";
"charger.editor.default_name" = "New Charger";
"charger.editor.field.current" = "Charge Current";
"charger.editor.field.input_voltage" = "Input Voltage";
"charger.editor.field.name" = "Name";
"charger.editor.field.output_voltage" = "Output Voltage";
"charger.editor.field.power" = "Charge Power";
"charger.editor.field.power.footer" = "Leave blank when the rated wattage isn't published. We'll calculate it from voltage and current.";
"charger.editor.placeholder.name" = "Workshop Charger";
"charger.editor.section.electrical" = "Electrical";
"charger.editor.section.power" = "Charge Output";
"charger.editor.title" = "Charger";
"chargers.badge.current" = "Current";
"chargers.badge.input" = "Input";
"chargers.badge.output" = "Output";
"chargers.badge.power" = "Power";
"chargers.onboarding.primary" = "Create Charger";
"chargers.onboarding.subtitle" = "Track shore power supplies, alternator chargers, and solar controllers to understand your charging capacity.";
"chargers.onboarding.title" = "Add your chargers";
"chargers.subtitle" = "Charger components will be available soon.";
"chargers.summary.metric.count" = "Chargers";
"chargers.summary.metric.current" = "Charge Rate";
"chargers.summary.metric.output" = "Output Voltage";
"chargers.summary.metric.power" = "Charge Power";
"chargers.summary.title" = "Charging Overview";
"chargers.title" = "Chargers for %@";
"component.fallback.name" = "Component";
"default.load.library" = "Library Load";
"default.load.name" = "My Load";
"default.load.unnamed" = "Unnamed Load";
"default.load.new" = "New Load";
"default.load.unnamed" = "Unnamed Load";
"default.system.name" = "My System";
"default.system.new" = "New System";
"editor.load.name_field" = "Load name";
@@ -26,167 +152,129 @@
"editor.system.location.optional" = "Location (optional)";
"editor.system.name_field" = "System name";
"editor.system.title" = "Edit System";
"loads.library.button" = "Library";
"loads.metric.cable" = "Cable";
"loads.metric.fuse" = "Fuse";
"loads.metric.length" = "Length";
"loads.onboarding.subtitle" = "Bring your system to life with components and let **Cable by VoltPlan** handle cable and fuse recommendations.";
"loads.onboarding.title" = "Add your first component";
"loads.overview.empty.create" = "Add Load";
"loads.overview.empty.library" = "Browse Library";
"loads.overview.empty.message" = "Start by adding a load to see system insights.";
"loads.overview.header.title" = "Load Overview";
"loads.overview.metric.count" = "Loads";
"loads.overview.metric.current" = "Total Current";
"loads.overview.metric.power" = "Total Power";
"loads.overview.status.missing_details.banner" = "Finish configuring your loads";
"loads.overview.status.missing_details.message" = "Enter cable length and wire size for %d %@ to see accurate recommendations.";
"loads.overview.status.missing_details.plural" = "loads";
"loads.overview.status.missing_details.singular" = "load";
"loads.overview.status.missing_details.title" = "Missing load details";
"overview.chargers.empty.create" = "Add Charger";
"overview.chargers.empty.subtitle" = "Add shore power, DC-DC, or solar chargers to understand your charging capacity.";
"overview.chargers.empty.title" = "No chargers configured yet";
"overview.chargers.header.title" = "Charger Overview";
"overview.loads.empty.subtitle" = "Add components to get cable sizing and fuse recommendations tailored to this system.";
"overview.loads.empty.title" = "No loads configured yet";
"overview.runtime.subtitle" = "At maximum load draw";
"overview.runtime.title" = "Estimated runtime";
"overview.runtime.unavailable" = "Add battery capacity and load power to estimate runtime.";
"overview.system.header.title" = "System Overview";
"sample.charger.dcdc.name" = "DC-DC charger";
"sample.charger.shore.name" = "Shore power charger";
"sample.charger.workbench.name" = "Workbench charger";
"sample.load.charger.name" = "Tool charger";
"sample.load.compressor.name" = "Air compressor";
"sample.load.fridge.name" = "Compressor fridge";
"sample.load.lighting.name" = "LED strip lighting";
"sample.system.rv.location" = "12V living circuit";
"sample.system.rv.name" = "Adventure Van";
"sample.system.workshop.location" = "Tool corner";
"sample.system.workshop.name" = "Workshop Bench";
"slider.button.ampere" = "Ampere";
"slider.button.watt" = "Watt";
"slider.current.title" = "Current";
"slider.length.title" = "Cable Length (%@)";
"slider.power.title" = "Power";
"slider.voltage.title" = "Voltage";
"system.list.no.components" = "No components yet";
"units.imperial.display" = "Imperial (AWG, ft)";
"units.metric.display" = "Metric (mm², m)";
"sample.system.rv.name" = "Adventure Van";
"sample.system.rv.location" = "12V living circuit";
"sample.system.workshop.name" = "Workshop Bench";
"sample.system.workshop.location" = "Tool corner";
"sample.load.fridge.name" = "Compressor fridge";
"sample.load.lighting.name" = "LED strip lighting";
"sample.load.compressor.name" = "Air compressor";
"sample.load.charger.name" = "Tool charger";
"system.icon.keywords.rv" = "rv, van, camper, motorhome, coach";
"system.icon.keywords.truck" = "truck, trailer, rig";
"system.icon.keywords.boat" = "boat, marine, yacht, sail";
"system.icon.keywords.plane" = "plane, air, flight";
"system.icon.keywords.ferry" = "ferry, ship";
"system.icon.keywords.house" = "house, home, cabin, cottage, lodge";
"system.icon.keywords.building" = "building, office, warehouse, factory, facility";
"system.icon.keywords.tent" = "camp, tent, outdoor";
"system.icon.keywords.solar" = "solar, sun";
"system.icon.keywords.battery" = "battery, storage";
"system.icon.keywords.server" = "server, data, network, rack";
"system.icon.keywords.computer" = "computer, electronics, lab, tech";
"system.icon.keywords.gear" = "gear, mechanic, machine, workshop";
"system.icon.keywords.tool" = "tool, maintenance, repair, shop";
"system.icon.keywords.hammer" = "hammer, carpentry";
"system.icon.keywords.light" = "light, lighting, lamp";
"system.icon.keywords.boat" = "boat, marine, yacht, sail";
"system.icon.keywords.bolt" = "bolt, power, electric";
"system.icon.keywords.plug" = "plug";
"system.icon.keywords.engine" = "engine, generator, motor";
"system.icon.keywords.fuel" = "fuel, diesel, gas";
"system.icon.keywords.water" = "water, pump, tank";
"system.icon.keywords.heat" = "heat, heater, furnace";
"system.icon.keywords.cold" = "cold, freeze, cool";
"system.icon.keywords.building" = "building, office, warehouse, factory, facility";
"system.icon.keywords.climate" = "climate, hvac, temperature";
"tab.overview" = "Overview";
"tab.components" = "Components";
"system.icon.keywords.cold" = "cold, freeze, cool";
"system.icon.keywords.computer" = "computer, electronics, lab, tech";
"system.icon.keywords.engine" = "engine, generator, motor";
"system.icon.keywords.ferry" = "ferry, ship";
"system.icon.keywords.fuel" = "fuel, diesel, gas";
"system.icon.keywords.gear" = "gear, mechanic, machine, workshop";
"system.icon.keywords.hammer" = "hammer, carpentry";
"system.icon.keywords.heat" = "heat, heater, furnace";
"system.icon.keywords.house" = "house, home, cabin, cottage, lodge";
"system.icon.keywords.light" = "light, lighting, lamp";
"system.icon.keywords.plane" = "plane, air, flight";
"system.icon.keywords.plug" = "plug";
"system.icon.keywords.rv" = "rv, van, camper, motorhome, coach";
"system.icon.keywords.server" = "server, data, network, rack";
"system.icon.keywords.solar" = "solar, sun";
"system.icon.keywords.tent" = "camp, tent, outdoor";
"system.icon.keywords.tool" = "tool, maintenance, repair, shop";
"system.icon.keywords.truck" = "truck, trailer, rig";
"system.icon.keywords.water" = "water, pump, tank";
"system.list.no.components" = "No components yet";
"tab.batteries" = "Batteries";
"tab.chargers" = "Chargers";
"loads.overview.header.title" = "Load Overview";
"loads.overview.metric.count" = "Loads";
"loads.overview.metric.current" = "Total Current";
"loads.overview.metric.power" = "Total Power";
"loads.overview.empty.message" = "Start by adding a load to see system insights.";
"loads.overview.empty.create" = "Add Load";
"loads.overview.empty.library" = "Browse Library";
"loads.library.button" = "Library";
"loads.onboarding.title" = "Add your first component";
"loads.onboarding.subtitle" = "Bring your system to life with components and let **Cable by VoltPlan** handle cable and fuse recommendations.";
"loads.overview.status.missing_details.title" = "Missing load details";
"loads.overview.status.missing_details.message" = "Enter cable length and wire size for %d %@ to see accurate recommendations.";
"loads.overview.status.missing_details.singular" = "load";
"loads.overview.status.missing_details.plural" = "loads";
"loads.overview.status.missing_details.banner" = "Finish configuring your loads";
"loads.metric.fuse" = "Fuse";
"loads.metric.cable" = "Cable";
"loads.metric.length" = "Length";
"overview.system.header.title" = "System Overview";
"overview.loads.empty.title" = "No loads configured yet";
"overview.loads.empty.subtitle" = "Add components to get cable sizing and fuse recommendations tailored to this system.";
"overview.runtime.title" = "Estimated runtime";
"overview.runtime.subtitle" = "At maximum load draw";
"overview.runtime.unavailable" = "Add battery capacity and load power to estimate runtime.";
"battery.bank.warning.voltage.short" = "Voltage";
"battery.bank.warning.capacity.short" = "Capacity";
"battery.bank.header.title" = "Battery Bank";
"battery.bank.metric.count" = "Batteries";
"battery.bank.metric.capacity" = "Capacity";
"battery.bank.metric.energy" = "Energy";
"battery.overview.empty.create" = "Add Battery";
"battery.onboarding.title" = "Add your first battery";
"battery.onboarding.subtitle" = "Track your bank's capacity and chemistry to keep runtime expectations in check.";
"battery.bank.badge.voltage" = "Voltage";
"overview.chargers.header.title" = "Charger Overview";
"overview.chargers.empty.title" = "No chargers configured yet";
"overview.chargers.empty.subtitle" = "Add shore power, DC-DC, or solar chargers to understand your charging capacity.";
"overview.chargers.empty.create" = "Add Charger";
"battery.bank.badge.capacity" = "Capacity";
"battery.bank.badge.energy" = "Energy";
"battery.bank.banner.voltage" = "Voltage mismatch detected";
"battery.bank.banner.capacity" = "Capacity mismatch detected";
"battery.bank.empty.title" = "No Batteries Yet";
"battery.bank.empty.subtitle" = "Tap the plus button to configure a battery for %@.";
"battery.bank.status.dismiss" = "Got it";
"battery.bank.status.single.battery" = "One battery";
"battery.bank.status.multiple.batteries" = "%d batteries";
"battery.bank.status.voltage.title" = "Voltage mismatch";
"battery.bank.status.voltage.message" = "%@ diverges from the bank baseline of %@. Mixing nominal voltages leads to uneven charging and can damage connected chargers or inverters.";
"battery.bank.status.capacity.title" = "Capacity mismatch";
"battery.bank.status.capacity.message" = "%@ uses a different capacity than the dominant bank size of %@. Mismatched capacities cause uneven discharge and premature wear.";
"battery.editor.title" = "Battery Setup";
"battery.editor.cancel" = "Cancel";
"battery.editor.save" = "Save";
"battery.editor.field.name" = "Name";
"battery.editor.placeholder.name" = "House Bank";
"battery.editor.field.chemistry" = "Chemistry";
"battery.editor.section.summary" = "Summary";
"battery.editor.slider.voltage" = "Nominal Voltage";
"battery.editor.slider.capacity" = "Capacity";
"battery.editor.alert.voltage.title" = "Edit Nominal Voltage";
"battery.editor.alert.voltage.placeholder" = "Voltage";
"battery.editor.alert.voltage.message" = "Enter voltage in volts (V)";
"battery.editor.alert.capacity.title" = "Edit Capacity";
"battery.editor.alert.capacity.placeholder" = "Capacity";
"battery.editor.alert.capacity.message" = "Enter capacity in amp-hours (Ah)";
"battery.editor.alert.cancel" = "Cancel";
"battery.editor.alert.save" = "Save";
"battery.editor.default_name" = "New Battery";
"charger.editor.title" = "Charger";
"charger.editor.field.name" = "Name";
"charger.editor.placeholder.name" = "Workshop Charger";
"charger.editor.section.electrical" = "Electrical";
"charger.editor.section.power" = "Charge Output";
"charger.editor.appearance.title" = "Charger Appearance";
"charger.editor.appearance.subtitle" = "Customize how this charger shows up";
"charger.editor.appearance.accessibility" = "Edit charger appearance";
"charger.editor.field.input_voltage" = "Input Voltage";
"charger.editor.field.output_voltage" = "Output Voltage";
"charger.editor.field.current" = "Charge Current";
"charger.editor.field.power" = "Charge Power";
"charger.editor.field.power.footer" = "Leave blank when the rated wattage isn't published. We'll calculate it from voltage and current.";
"charger.editor.default_name" = "New Charger";
"charger.editor.alert.input_voltage.title" = "Edit Input Voltage";
"charger.editor.alert.output_voltage.title" = "Edit Output Voltage";
"charger.editor.alert.current.title" = "Edit Charge Current";
"charger.editor.alert.voltage.message" = "Enter voltage in volts (V)";
"charger.editor.alert.power.title" = "Edit Charge Power";
"charger.editor.alert.power.placeholder" = "Power";
"charger.editor.alert.power.message" = "Enter power in watts (W)";
"charger.editor.alert.current.message" = "Enter current in amps (A)";
"charger.editor.alert.cancel" = "Cancel";
"charger.editor.alert.save" = "Save";
"charger.default.new" = "New Charger";
"chargers.summary.title" = "Charging Overview";
"chargers.summary.metric.count" = "Chargers";
"chargers.summary.metric.output" = "Output Voltage";
"chargers.summary.metric.current" = "Charge Rate";
"chargers.summary.metric.power" = "Charge Power";
"chargers.badge.input" = "Input";
"chargers.badge.output" = "Output";
"chargers.badge.current" = "Current";
"chargers.badge.power" = "Power";
"chargers.onboarding.title" = "Add your chargers";
"chargers.onboarding.subtitle" = "Track shore power supplies, alternator chargers, and solar controllers to understand your charging capacity.";
"chargers.onboarding.primary" = "Create Charger";
"sample.charger.shore.name" = "Shore power charger";
"sample.charger.dcdc.name" = "DC-DC charger";
"sample.charger.workbench.name" = "Workbench charger";
"chargers.title" = "Chargers for %@";
"chargers.subtitle" = "Charger components will be available soon.";
"tab.components" = "Components";
"tab.overview" = "Overview";
"units.imperial.display" = "Imperial (AWG, ft)";
"units.metric.display" = "Metric (mm², m)";
"settings.pro.cta.description" = "Cable PRO keeps advanced calculations and early tools available.";
"settings.pro.cta.button" = "Get Cable PRO";
"settings.pro.renewal.date" = "Renews on %@.";
"settings.pro.trial.remaining" = "%@ remaining in free trial.";
"settings.pro.trial.today" = "Free trial renews today.";
"settings.pro.instructions" = "Manage or cancel your subscription in the App Store.";
"settings.pro.manage.button" = "Manage Subscription";
"settings.pro.manage.url" = "https://apps.apple.com/account/subscriptions";
"settings.pro.day.one" = "%@ day";
"settings.pro.day.other" = "%@ days";
"cable.pro.terms.label" = "Terms";
"cable.pro.privacy.label" = "Privacy";
"cable.pro.terms.url" = "https://voltplan.app/terms";
"cable.pro.privacy.url" = "https://voltplan.app/privacy";
"cable.pro.paywall.title" = "Cable PRO";
"cable.pro.paywall.subtitle" = "Cable PRO enables more configuration options for loads, batteries and chargers.";
"cable.pro.feature.dutyCycle" = "Duty-cycle aware cable calculators";
"cable.pro.feature.batteryCapacity" = "Configure usable battery capacity";
"cable.pro.feature.usageBased" = "Usage based calculations";
"cable.pro.button.unlock" = "Unlock Now";
"cable.pro.button.freeTrial" = "Start Free Trial";
"cable.pro.button.unlocked" = "Unlocked";
"cable.pro.restore.button" = "Restore Purchases";
"cable.pro.alert.success.title" = "Cable PRO Unlocked";
"cable.pro.alert.success.body" = "Thanks for supporting Cable PRO!";
"cable.pro.alert.pending.title" = "Purchase Pending";
"cable.pro.alert.pending.body" = "Your purchase is awaiting approval.";
"cable.pro.alert.restored.title" = "Purchases Restored";
"cable.pro.alert.restored.body" = "Your purchases are available again.";
"cable.pro.alert.error.title" = "Purchase Failed";
"cable.pro.alert.error.generic" = "Something went wrong. Please try again.";
"cable.pro.trial.badge" = "Includes a %@ free trial";
"cable.pro.subscription.renews" = "Renews %@.";
"cable.pro.subscription.trialThenRenews" = "Free trial, then renews %@.";
"cable.pro.duration.day.singular" = "every day";
"cable.pro.duration.day.plural" = "every %@ days";
"cable.pro.duration.week.singular" = "every week";
"cable.pro.duration.week.plural" = "every %@ weeks";
"cable.pro.duration.month.singular" = "every month";
"cable.pro.duration.month.plural" = "every %@ months";
"cable.pro.duration.year.singular" = "every year";
"cable.pro.duration.year.plural" = "every %@ years";
"cable.pro.trial.duration.day.singular" = "%@-day";
"cable.pro.trial.duration.day.plural" = "%@-day";
"cable.pro.trial.duration.week.singular" = "%@-week";
"cable.pro.trial.duration.week.plural" = "%@-week";
"cable.pro.trial.duration.month.singular" = "%@-month";
"cable.pro.trial.duration.month.plural" = "%@-month";
"cable.pro.trial.duration.year.singular" = "%@-year";
"cable.pro.trial.duration.year.plural" = "%@-year";

View File

@@ -86,6 +86,14 @@ struct BatteriesView: View {
)
}
private var metricUsableCapacityLabel: String {
String(
localized: "battery.bank.metric.usable_capacity",
bundle: .main,
comment: "Label for usable capacity metric"
)
}
private var badgeVoltageLabel: String {
String(
localized: "battery.bank.badge.voltage",
@@ -110,6 +118,14 @@ struct BatteriesView: View {
)
}
private var badgeUsableCapacityLabel: String {
String(
localized: "battery.bank.metric.usable_capacity",
bundle: .main,
comment: "Label for usable capacity badge"
)
}
private var emptyTitle: String {
String(
localized: "battery.bank.empty.title",
@@ -190,49 +206,15 @@ struct BatteriesView: View {
Spacer()
}
ViewThatFits(in: .horizontal) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
summaryMetric(
icon: "battery.100",
label: metricCountLabel,
value: "\(batteries.count)",
tint: .blue
)
summaryMetric(
icon: "gauge.medium",
label: metricCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
)
summaryMetric(
icon: "bolt.circle",
label: metricEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
)
}
VStack(alignment: .leading, spacing: 12) {
summaryMetric(
icon: "battery.100",
label: metricCountLabel,
value: "\(batteries.count)",
tint: .blue
)
summaryMetric(
icon: "gauge.medium",
label: metricCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
)
summaryMetric(
icon: "bolt.circle",
label: metricEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
)
ForEach(Array(summaryMetrics.enumerated()), id: \.offset) { _, metric in
summaryMetric(icon: metric.icon, label: metric.label, value: metric.value, tint: metric.tint)
}
}
.padding(.horizontal, 2)
}
.scrollClipDisabled(false)
if let status = bankStatus {
Button {
@@ -285,24 +267,7 @@ struct BatteriesView: View {
.foregroundStyle(.secondary)
}
HStack(spacing: 12) {
metricBadge(
label: badgeVoltageLabel,
value: formattedValue(battery.nominalVoltage, unit: "V"),
tint: .orange
)
metricBadge(
label: badgeCapacityLabel,
value: formattedValue(battery.capacityAmpHours, unit: "Ah"),
tint: .blue
)
metricBadge(
label: badgeEnergyLabel,
value: formattedValue(battery.energyWattHours, unit: "Wh"),
tint: .green
)
Spacer()
}
batteryMetricsScroll(for: battery)
}
.padding(.vertical, 16)
.padding(.horizontal, 16)
@@ -335,6 +300,27 @@ struct BatteriesView: View {
}
}
private var totalUsableCapacity: Double {
batteries.reduce(0) { result, battery in
result + battery.usableCapacityAmpHours
}
}
private var totalUsableCapacityShare: Double {
guard totalCapacity > 0 else { return 0 }
return max(0, min(1, totalUsableCapacity / totalCapacity))
}
private func usableFraction(for battery: SavedBattery) -> Double {
guard battery.capacityAmpHours > 0 else { return 0 }
return max(0, min(1, battery.usableCapacityAmpHours / battery.capacityAmpHours))
}
private func usableCapacityDisplay(for battery: SavedBattery) -> String {
let fraction = usableFraction(for: battery)
return "\(formattedValue(battery.usableCapacityAmpHours, unit: "Ah")) (\(formattedPercentage(fraction)))"
}
private func summaryMetric(icon: String, label: String, value: String, tint: Color) -> some View {
ComponentSummaryMetricView(
icon: icon,
@@ -344,6 +330,55 @@ struct BatteriesView: View {
)
}
private var summaryMetrics: [(icon: String, label: String, value: String, tint: Color)] {
[
(
icon: "battery.100",
label: metricCountLabel,
value: "\(batteries.count)",
tint: .blue
),
(
icon: "gauge.medium",
label: metricCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
),
(
icon: "battery.100.bolt",
label: metricUsableCapacityLabel,
value: "\(formattedValue(totalUsableCapacity, unit: "Ah")) (\(formattedPercentage(totalUsableCapacityShare)))",
tint: .purple
),
(
icon: "bolt.circle",
label: metricEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
)
]
}
@ViewBuilder
private func batteryMetricsScroll(for battery: SavedBattery) -> some View {
let badges: [(String, String, Color)] = [
(badgeVoltageLabel, formattedValue(battery.nominalVoltage, unit: "V"), .orange),
(badgeCapacityLabel, formattedValue(battery.capacityAmpHours, unit: "Ah"), .blue),
(badgeUsableCapacityLabel, usableCapacityDisplay(for: battery), .purple),
(badgeEnergyLabel, formattedValue(battery.energyWattHours, unit: "Wh"), .green)
]
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(Array(badges.enumerated()), id: \.offset) { _, badge in
ComponentMetricBadgeView(label: badge.0, value: badge.1, tint: badge.2)
}
}
.padding(.horizontal, 2)
}
.scrollClipDisabled(false)
}
private func metricBadge(label: String, value: String, tint: Color) -> some View {
ComponentMetricBadgeView(
label: label,
@@ -401,6 +436,13 @@ struct BatteriesView: View {
return "\(numberString) \(unit)"
}
private func formattedPercentage(_ fraction: Double) -> String {
let clamped = max(0, min(1, fraction))
let percent = clamped * 100
let numberString = Self.numberFormatter.string(from: NSNumber(value: percent)) ?? String(format: "%.1f", percent)
return "\(numberString) %"
}
private var dominantVoltage: Double? {
guard batteries.count > 1 else { return nil }
return dominantValue(

View File

@@ -14,12 +14,32 @@ struct BatteryConfiguration: Identifiable, Hashable {
var displayName: String {
rawValue
}
var usableCapacityFraction: Double {
switch self {
case .floodedLeadAcid:
return 0.5
case .agm:
return 0.5
case .gel:
return 0.6
case .lithiumIronPhosphate:
return 0.9
case .lithiumIon:
return 0.85
}
}
}
let id: UUID
var name: String
var nominalVoltage: Double
var capacityAmpHours: Double
var usableCapacityOverrideFraction: Double?
var chargeVoltage: Double
var cutOffVoltage: Double
var minimumTemperatureCelsius: Double
var maximumTemperatureCelsius: Double
var chemistry: Chemistry
var iconName: String
var colorName: String
@@ -31,6 +51,11 @@ struct BatteryConfiguration: Identifiable, Hashable {
nominalVoltage: Double = 12.8,
capacityAmpHours: Double = 100,
chemistry: Chemistry = .lithiumIronPhosphate,
usableCapacityOverrideFraction: Double? = nil,
chargeVoltage: Double = 14.4,
cutOffVoltage: Double = 10.8,
minimumTemperatureCelsius: Double = -20,
maximumTemperatureCelsius: Double = 60,
iconName: String = "battery.100",
colorName: String = "blue",
system: ElectricalSystem
@@ -39,6 +64,11 @@ struct BatteryConfiguration: Identifiable, Hashable {
self.name = name
self.nominalVoltage = nominalVoltage
self.capacityAmpHours = capacityAmpHours
self.usableCapacityOverrideFraction = usableCapacityOverrideFraction
self.chargeVoltage = chargeVoltage
self.cutOffVoltage = cutOffVoltage
self.minimumTemperatureCelsius = minimumTemperatureCelsius
self.maximumTemperatureCelsius = maximumTemperatureCelsius
self.chemistry = chemistry
self.iconName = iconName
self.colorName = colorName
@@ -50,6 +80,17 @@ struct BatteryConfiguration: Identifiable, Hashable {
self.name = savedBattery.name
self.nominalVoltage = savedBattery.nominalVoltage
self.capacityAmpHours = savedBattery.capacityAmpHours
self.usableCapacityOverrideFraction = savedBattery.usableCapacityOverrideFraction
self.chargeVoltage = savedBattery.chargeVoltage ?? 14.4
self.cutOffVoltage = savedBattery.cutOffVoltage ?? 10.8
self.minimumTemperatureCelsius = savedBattery.minimumTemperatureCelsius ?? -20
self.maximumTemperatureCelsius = savedBattery.maximumTemperatureCelsius ?? 60
if self.maximumTemperatureCelsius < self.minimumTemperatureCelsius {
let correctedMin = min(self.minimumTemperatureCelsius, self.maximumTemperatureCelsius)
let correctedMax = max(self.minimumTemperatureCelsius, self.maximumTemperatureCelsius)
self.minimumTemperatureCelsius = correctedMin
self.maximumTemperatureCelsius = correctedMax
}
self.chemistry = savedBattery.chemistry
self.iconName = savedBattery.iconName
self.colorName = savedBattery.colorName
@@ -60,10 +101,38 @@ struct BatteryConfiguration: Identifiable, Hashable {
nominalVoltage * capacityAmpHours
}
var defaultUsableCapacityFraction: Double {
chemistry.usableCapacityFraction
}
var usableCapacityFraction: Double {
if let override = usableCapacityOverrideFraction {
return max(0, min(1, override))
}
return defaultUsableCapacityFraction
}
var defaultUsableCapacityAmpHours: Double {
capacityAmpHours * defaultUsableCapacityFraction
}
var usableCapacityAmpHours: Double {
capacityAmpHours * usableCapacityFraction
}
var usableEnergyWattHours: Double {
usableCapacityAmpHours * nominalVoltage
}
func apply(to savedBattery: SavedBattery) {
savedBattery.name = name
savedBattery.nominalVoltage = nominalVoltage
savedBattery.capacityAmpHours = capacityAmpHours
savedBattery.usableCapacityOverrideFraction = usableCapacityOverrideFraction
savedBattery.chargeVoltage = chargeVoltage
savedBattery.cutOffVoltage = cutOffVoltage
savedBattery.minimumTemperatureCelsius = minimumTemperatureCelsius
savedBattery.maximumTemperatureCelsius = maximumTemperatureCelsius
savedBattery.chemistry = chemistry
savedBattery.iconName = iconName
savedBattery.colorName = colorName
@@ -78,6 +147,11 @@ extension BatteryConfiguration {
lhs.name == rhs.name &&
lhs.nominalVoltage == rhs.nominalVoltage &&
lhs.capacityAmpHours == rhs.capacityAmpHours &&
lhs.usableCapacityOverrideFraction == rhs.usableCapacityOverrideFraction &&
lhs.chargeVoltage == rhs.chargeVoltage &&
lhs.cutOffVoltage == rhs.cutOffVoltage &&
lhs.minimumTemperatureCelsius == rhs.minimumTemperatureCelsius &&
lhs.maximumTemperatureCelsius == rhs.maximumTemperatureCelsius &&
lhs.chemistry == rhs.chemistry &&
lhs.iconName == rhs.iconName &&
lhs.colorName == rhs.colorName
@@ -88,6 +162,11 @@ extension BatteryConfiguration {
hasher.combine(name)
hasher.combine(nominalVoltage)
hasher.combine(capacityAmpHours)
hasher.combine(usableCapacityOverrideFraction)
hasher.combine(chargeVoltage)
hasher.combine(cutOffVoltage)
hasher.combine(minimumTemperatureCelsius)
hasher.combine(maximumTemperatureCelsius)
hasher.combine(chemistry)
hasher.combine(iconName)
hasher.combine(colorName)

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,8 @@ class CableCalculator: ObservableObject {
@Published var power: Double = 60.0
@Published var length: Double = 10.0
@Published var loadName: String = String(localized: "default.load.name", comment: "Default placeholder name for a load")
@Published var dutyCyclePercent: Double = 100.0
@Published var dailyUsageHours: Double = 1.0
var calculatedPower: Double {
voltage * current
@@ -132,6 +134,8 @@ class SavedLoad {
var iconName: String = "lightbulb"
var colorName: String = "blue"
var isWattMode: Bool = false
var dutyCyclePercent: Double = 100.0
var dailyUsageHours: Double = 1.0
var system: ElectricalSystem?
var remoteIconURLString: String? = nil
var affiliateURLString: String? = nil
@@ -139,7 +143,25 @@ class SavedLoad {
var bomCompletedItemIDs: [String] = []
var identifier: String = UUID().uuidString
init(name: String, voltage: Double, current: Double, power: Double, length: Double, crossSection: Double, iconName: String = "lightbulb", colorName: String = "blue", isWattMode: Bool = false, system: ElectricalSystem? = nil, remoteIconURLString: String? = nil, affiliateURLString: String? = nil, affiliateCountryCode: String? = nil, bomCompletedItemIDs: [String] = [], identifier: String = UUID().uuidString) {
init(
name: String,
voltage: Double,
current: Double,
power: Double,
length: Double,
crossSection: Double,
iconName: String = "lightbulb",
colorName: String = "blue",
isWattMode: Bool = false,
dutyCyclePercent: Double = 100.0,
dailyUsageHours: Double = 1.0,
system: ElectricalSystem? = nil,
remoteIconURLString: String? = nil,
affiliateURLString: String? = nil,
affiliateCountryCode: String? = nil,
bomCompletedItemIDs: [String] = [],
identifier: String = UUID().uuidString
) {
self.name = name
self.voltage = voltage
self.current = current
@@ -150,6 +172,8 @@ class SavedLoad {
self.iconName = iconName
self.colorName = colorName
self.isWattMode = isWattMode
self.dutyCyclePercent = dutyCyclePercent
self.dailyUsageHours = dailyUsageHours
self.system = system
self.remoteIconURLString = remoteIconURLString
self.affiliateURLString = affiliateURLString

View File

@@ -22,9 +22,14 @@ struct CalculatorView: View {
@State private var currentInput: String = ""
@State private var powerInput: String = ""
@State private var lengthInput: String = ""
@State private var dutyCycleInput: String = ""
@State private var usageHoursInput: String = ""
@State private var showingLoadEditor = false
@State private var showingProUpsell = false
@State private var presentedAffiliateLink: AffiliateLinkInfo?
@State private var completedItemIDs: Set<String>
@State private var isAdvancedExpanded = false
@State private var hasActiveProSubscription = false
let savedLoad: SavedLoad?
@@ -34,7 +39,7 @@ struct CalculatorView: View {
}
enum EditingValue {
case voltage, current, power, length
case voltage, current, power, length, dutyCycle, usageHours
}
private static let editFormatter: NumberFormatter = {
@@ -69,72 +74,27 @@ struct CalculatorView: View {
}
var body: some View {
VStack(spacing: 0) {
badgesSection
circuitDiagram
resultsSection
mainContent
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("")
.toolbar {
ToolbarItem(placement: .principal) {
navigationTitle
}
if savedLoad == nil {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
saveCurrentLoad()
}
}
}
}
.sheet(isPresented: $showingLibrary) {
LoadLibraryView(calculator: calculator)
}
.sheet(isPresented: $showingLoadEditor) {
LoadEditorView(
loadName: Binding(
get: { calculator.loadName },
set: {
calculator.loadName = $0
autoUpdateSavedLoad()
}
),
iconName: Binding(
get: { savedLoad?.iconName ?? "lightbulb" },
set: { newValue in
guard let savedLoad else { return }
savedLoad.iconName = newValue
savedLoad.remoteIconURLString = nil
autoUpdateSavedLoad()
}
),
colorName: Binding(
get: { savedLoad?.colorName ?? "blue" },
set: {
savedLoad?.colorName = $0
autoUpdateSavedLoad()
}
),
remoteIconURLString: Binding(
get: { savedLoad?.remoteIconURLString },
set: { newValue in
guard let savedLoad else { return }
savedLoad.remoteIconURLString = newValue
autoUpdateSavedLoad()
}
)
)
}
.sheet(item: $presentedAffiliateLink) { info in
BillOfMaterialsView(
info: info,
items: buildBillOfMaterialsItems(deviceLink: info.affiliateURL),
completedItemIDs: $completedItemIDs
attachAlerts(
attachSheets(
navigationWrapped(mainLayout)
)
)
.task {
hasActiveProSubscription = (await SettingsView.fetchProStatus()) != nil
}
}
private func attachAlerts<V: View>(_ view: V) -> some View {
let withLength = addLengthAlert(to: view)
let withVoltage = addVoltageAlert(to: withLength)
let withCurrent = addCurrentAlert(to: withVoltage)
let withPower = addPowerAlert(to: withCurrent)
let withDutyCycle = addDutyCycleAlert(to: withPower)
return addUsageHoursAlert(to: withDutyCycle)
}
private func addLengthAlert<V: View>(to view: V) -> some View {
view
.alert("Edit Length", isPresented: Binding(
get: { editingValue == .length },
set: { isPresented in
@@ -151,10 +111,10 @@ struct CalculatorView: View {
lengthInput = formattedValue(calculator.length)
}
}
.onChange(of: lengthInput) { _, newValue in
guard editingValue == .length, let parsed = parseInput(newValue) else { return }
calculator.length = roundToTenth(parsed)
}
.onChange(of: lengthInput) { _, newValue in
guard editingValue == .length, let parsed = parseInput(newValue) else { return }
calculator.length = roundToTenth(parsed)
}
Button("Cancel", role: .cancel) {
editingValue = nil
lengthInput = ""
@@ -171,6 +131,10 @@ struct CalculatorView: View {
} message: {
Text("Enter length in \(unitSettings.unitSystem.lengthUnit)")
}
}
private func addVoltageAlert<V: View>(to view: V) -> some View {
view
.alert("Edit Voltage", isPresented: Binding(
get: { editingValue == .voltage },
set: { isPresented in
@@ -187,10 +151,10 @@ struct CalculatorView: View {
voltageInput = formattedValue(calculator.voltage)
}
}
.onChange(of: voltageInput) { _, newValue in
guard editingValue == .voltage, let parsed = parseInput(newValue) else { return }
calculator.voltage = roundToTenth(parsed)
}
.onChange(of: voltageInput) { _, newValue in
guard editingValue == .voltage, let parsed = parseInput(newValue) else { return }
calculator.voltage = roundToTenth(parsed)
}
Button("Cancel", role: .cancel) {
editingValue = nil
voltageInput = ""
@@ -212,6 +176,10 @@ struct CalculatorView: View {
} message: {
Text("Enter voltage in volts (V)")
}
}
private func addCurrentAlert<V: View>(to view: V) -> some View {
view
.alert("Edit Current", isPresented: Binding(
get: { editingValue == .current },
set: { isPresented in
@@ -228,10 +196,10 @@ struct CalculatorView: View {
currentInput = formattedValue(calculator.current)
}
}
.onChange(of: currentInput) { _, newValue in
guard editingValue == .current, let parsed = parseInput(newValue) else { return }
calculator.current = roundToTenth(parsed)
}
.onChange(of: currentInput) { _, newValue in
guard editingValue == .current, let parsed = parseInput(newValue) else { return }
calculator.current = roundToTenth(parsed)
}
Button("Cancel", role: .cancel) {
editingValue = nil
currentInput = ""
@@ -249,6 +217,10 @@ struct CalculatorView: View {
} message: {
Text("Enter current in amperes (A)")
}
}
private func addPowerAlert<V: View>(to view: V) -> some View {
view
.alert("Edit Power", isPresented: Binding(
get: { editingValue == .power },
set: { isPresented in
@@ -265,10 +237,10 @@ struct CalculatorView: View {
powerInput = formattedValue(calculator.power)
}
}
.onChange(of: powerInput) { _, newValue in
guard editingValue == .power, let parsed = parseInput(newValue) else { return }
calculator.power = roundToNearestFive(parsed)
}
.onChange(of: powerInput) { _, newValue in
guard editingValue == .power, let parsed = parseInput(newValue) else { return }
calculator.power = roundToNearestFive(parsed)
}
Button("Cancel", role: .cancel) {
editingValue = nil
powerInput = ""
@@ -286,14 +258,192 @@ struct CalculatorView: View {
} message: {
Text("Enter power in watts (W)")
}
.onAppear {
if let savedLoad = savedLoad {
loadConfiguration(from: savedLoad)
}
private func addDutyCycleAlert<V: View>(to view: V) -> some View {
view
.alert(
dutyCycleAlertTitle,
isPresented: Binding(
get: { editingValue == .dutyCycle },
set: { isPresented in
if !isPresented {
editingValue = nil
dutyCycleInput = ""
}
}
)
) {
TextField(dutyCycleAlertPlaceholder, text: $dutyCycleInput)
.keyboardType(.decimalPad)
.onAppear {
if dutyCycleInput.isEmpty {
dutyCycleInput = formattedValue(calculator.dutyCyclePercent)
}
}
.onChange(of: dutyCycleInput) { _, newValue in
guard editingValue == .dutyCycle, let parsed = parseInput(newValue) else { return }
calculator.dutyCyclePercent = clampDutyCyclePercent(parsed)
}
Button("Cancel", role: .cancel) {
editingValue = nil
dutyCycleInput = ""
}
Button("Save") {
if let parsed = parseInput(dutyCycleInput) {
calculator.dutyCyclePercent = clampDutyCyclePercent(parsed)
}
editingValue = nil
dutyCycleInput = ""
calculator.objectWillChange.send()
autoUpdateSavedLoad()
}
} message: {
Text(dutyCycleAlertMessage)
}
}
private func addUsageHoursAlert<V: View>(to view: V) -> some View {
view
.alert(
usageHoursAlertTitle,
isPresented: Binding(
get: { editingValue == .usageHours },
set: { isPresented in
if !isPresented {
editingValue = nil
usageHoursInput = ""
}
}
)
) {
TextField(usageHoursAlertPlaceholder, text: $usageHoursInput)
.keyboardType(.decimalPad)
.onAppear {
if usageHoursInput.isEmpty {
usageHoursInput = formattedValue(calculator.dailyUsageHours)
}
}
.onChange(of: usageHoursInput) { _, newValue in
guard editingValue == .usageHours, let parsed = parseInput(newValue) else { return }
calculator.dailyUsageHours = clampUsageHours(parsed)
}
Button("Cancel", role: .cancel) {
editingValue = nil
usageHoursInput = ""
}
Button("Save") {
if let parsed = parseInput(usageHoursInput) {
calculator.dailyUsageHours = clampUsageHours(parsed)
}
editingValue = nil
usageHoursInput = ""
calculator.objectWillChange.send()
autoUpdateSavedLoad()
}
} message: {
Text(usageHoursAlertMessage)
}
}
private func attachSheets<V: View>(_ view: V) -> some View {
view
.sheet(isPresented: $showingLibrary, content: librarySheet)
.sheet(isPresented: $showingLoadEditor, content: loadEditorSheet)
.sheet(item: $presentedAffiliateLink, content: billOfMaterialsSheet(info:))
.sheet(isPresented: $showingProUpsell) {
CableProPaywallView(isPresented: $showingProUpsell)
}
.onAppear {
if let savedLoad = savedLoad {
loadConfiguration(from: savedLoad)
}
}
.onChange(of: completedItemIDs) { _, _ in
persistCompletedItems()
}
}
private func navigationWrapped<V: View>(_ view: V) -> some View {
view
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("")
.toolbar { toolbarContent }
}
private var mainLayout: some View {
VStack(spacing: 0) {
badgesSection
circuitDiagram
resultsSection
mainContent
}
}
@ToolbarContentBuilder
private var toolbarContent: some ToolbarContent {
ToolbarItem(placement: .principal) {
navigationTitle
}
if savedLoad == nil {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
saveCurrentLoad()
}
}
}
.onChange(of: completedItemIDs) { _, _ in
persistCompletedItems()
}
}
@ViewBuilder
private func billOfMaterialsSheet(info: AffiliateLinkInfo) -> some View {
BillOfMaterialsView(
info: info,
items: buildBillOfMaterialsItems(deviceLink: info.affiliateURL),
completedItemIDs: $completedItemIDs
)
}
@ViewBuilder
private func librarySheet() -> some View {
LoadLibraryView(calculator: calculator)
}
@ViewBuilder
private func loadEditorSheet() -> some View {
LoadEditorView(
loadName: Binding(
get: { calculator.loadName },
set: {
calculator.loadName = $0
autoUpdateSavedLoad()
}
),
iconName: Binding(
get: { savedLoad?.iconName ?? "lightbulb" },
set: { newValue in
guard let savedLoad else { return }
savedLoad.iconName = newValue
savedLoad.remoteIconURLString = nil
autoUpdateSavedLoad()
}
),
colorName: Binding(
get: { savedLoad?.colorName ?? "blue" },
set: {
savedLoad?.colorName = $0
autoUpdateSavedLoad()
}
),
remoteIconURLString: Binding(
get: { savedLoad?.remoteIconURLString },
set: { newValue in
guard let savedLoad else { return }
savedLoad.remoteIconURLString = newValue
autoUpdateSavedLoad()
}
)
)
}
private var loadIcon: String {
@@ -596,15 +746,20 @@ struct CalculatorView: View {
}
private var mainContent: some View {
ScrollView {
VStack(spacing: 20) {
Divider().padding(.horizontal)
slidersSection
if let info = affiliateLinkInfo {
affiliateLinkSection(info: info)
}
List {
slidersSection
advancedSettingsSection
if let info = affiliateLinkInfo {
affiliateLinkSection(info: info)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 12, leading: 18, bottom: 12, trailing: 18))
}
}
.listStyle(.plain)
.scrollIndicators(.hidden)
.scrollContentBackground(.hidden)
.background(Color(.systemGroupedBackground))
}
@ViewBuilder
@@ -640,7 +795,6 @@ struct CalculatorView: View {
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
.padding(.horizontal)
}
private func buildBillOfMaterialsItems(deviceLink: URL?) -> [BOMItem] {
@@ -758,13 +912,174 @@ struct CalculatorView: View {
return items
}
private var advancedSettingsTitle: String {
String(
localized: "calculator.advanced.section.title",
comment: "Title for the advanced load settings section"
)
}
private var dutyCycleTitle: String {
String(
localized: "calculator.advanced.duty_cycle.title",
comment: "Title for the duty cycle slider"
)
}
private var dutyCycleHelperText: String {
String(
localized: "calculator.advanced.duty_cycle.helper",
comment: "Helper text explaining duty cycle"
)
}
private var usageHoursTitle: String {
String(
localized: "calculator.advanced.usage_hours.title",
comment: "Title for the daily usage slider"
)
}
private var usageHoursHelperText: String {
String(
localized: "calculator.advanced.usage_hours.helper",
comment: "Helper text explaining daily usage hours"
)
}
private var dutyCycleAlertTitle: String {
String(
localized: "calculator.alert.duty_cycle.title",
comment: "Title for the duty cycle edit alert"
)
}
private var dutyCycleAlertPlaceholder: String {
String(
localized: "calculator.alert.duty_cycle.placeholder",
comment: "Placeholder for the duty cycle alert text field"
)
}
private var dutyCycleAlertMessage: String {
String(
localized: "calculator.alert.duty_cycle.message",
comment: "Helper message for the duty cycle alert"
)
}
private var usageHoursAlertTitle: String {
String(
localized: "calculator.alert.usage_hours.title",
comment: "Title for the daily usage edit alert"
)
}
private var usageHoursAlertPlaceholder: String {
String(
localized: "calculator.alert.usage_hours.placeholder",
comment: "Placeholder for the daily usage alert text field"
)
}
private var usageHoursAlertMessage: String {
String(
localized: "calculator.alert.usage_hours.message",
comment: "Helper message for the daily usage alert"
)
}
private var advancedFeaturesEnabled: Bool {
unitSettings.isProUnlocked || hasActiveProSubscription
}
private var slidersSection: some View {
VStack(spacing: 30) {
Section {
voltageSlider
.listRowSeparator(.hidden)
currentPowerSlider
.listRowSeparator(.hidden)
lengthSlider
.listRowSeparator(.hidden)
}
.padding(.horizontal)
.listRowBackground(Color(.systemBackground))
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 12, leading: 18, bottom: 12, trailing: 18))
}
private var advancedSettingsSection: some View {
let advancedEnabled = advancedFeaturesEnabled
return Section {
Button {
withAnimation(.easeInOut(duration: 0.2)) {
isAdvancedExpanded.toggle()
}
} label: {
HStack(spacing: 8) {
Text(advancedSettingsTitle.uppercased())
.font(.caption2)
.fontWeight(.medium)
.foregroundStyle(.secondary)
Spacer()
Image(systemName: isAdvancedExpanded ? "chevron.up" : "chevron.down")
.font(.footnote.weight(.semibold))
.foregroundStyle(.secondary)
}
.padding(.vertical, 10)
.opacity(advancedEnabled ? 1 : 0.5)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.listRowBackground(Color(.secondarySystemBackground))
.listRowSeparator(.hidden)
if isAdvancedExpanded {
if !advancedEnabled {
upgradeToProCTA
.listRowSeparator(.hidden)
.listRowBackground(Color(.systemBackground))
}
VStack(alignment: .leading, spacing: 8) {
dutyCycleSlider
.listRowSeparator(.hidden)
.listRowBackground(Color(.systemBackground))
.opacity(advancedEnabled ? 1 : 0.35)
.allowsHitTesting(advancedEnabled)
Text(dutyCycleHelperText)
.font(.caption)
.foregroundStyle(.secondary)
.opacity(advancedEnabled ? 1 : 0.35)
}
VStack(alignment: .leading, spacing: 8) {
usageHoursSlider
.listRowSeparator(.hidden)
.listRowBackground(Color(.systemBackground))
.opacity(advancedEnabled ? 1 : 0.35)
.allowsHitTesting(advancedEnabled)
Text(usageHoursHelperText)
.font(.caption)
.foregroundStyle(.secondary)
.opacity(advancedEnabled ? 1 : 0.35)
}
}
}
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 12, leading: 18, bottom: 12, trailing: 18))
.animation(.easeInOut(duration: 0.2), value: isAdvancedExpanded)
}
private var upgradeToProCTA: some View {
Button {
showingProUpsell = true
} label: {
Text("Get Cable PRO")
.font(.callout.weight(.semibold))
.frame(maxWidth: .infinity)
.padding(.vertical, 6)
}
.buttonStyle(.borderedProminent)
}
private var voltageSliderRange: ClosedRange<Double> {
@@ -788,6 +1103,14 @@ struct CalculatorView: View {
return 0...upperBound
}
private var dutyCycleRange: ClosedRange<Double> {
0...100
}
private var usageHoursRange: ClosedRange<Double> {
0...24
}
private var voltageSnapValues: [Double] {
[3.3, 5.0, 6.0, 9.0, 12.0, 13.8, 24.0, 28.0, 48.0]
}
@@ -920,6 +1243,52 @@ struct CalculatorView: View {
}
}
private var dutyCycleSlider: some View {
SliderSection(
title: dutyCycleTitle,
value: Binding(
get: { calculator.dutyCyclePercent },
set: { newValue in
let clamped = clampDutyCyclePercent(newValue)
calculator.dutyCyclePercent = clamped
}
),
range: dutyCycleRange,
unit: "%",
tapAction: beginDutyCycleEditing
)
.onChange(of: calculator.dutyCyclePercent) { _, newValue in
let clamped = clampDutyCyclePercent(newValue)
if abs(clamped - newValue) > 0.0001 {
calculator.dutyCyclePercent = clamped
}
autoUpdateSavedLoad()
}
}
private var usageHoursSlider: some View {
SliderSection(
title: usageHoursTitle,
value: Binding(
get: { calculator.dailyUsageHours },
set: { newValue in
let clamped = clampUsageHours(newValue)
calculator.dailyUsageHours = clamped
}
),
range: usageHoursRange,
unit: String(localized: "calculator.advanced.usage_hours.unit", comment: "Unit label for usage hours slider"),
tapAction: beginUsageHoursEditing
)
.onChange(of: calculator.dailyUsageHours) { _, newValue in
let clamped = clampUsageHours(newValue)
if abs(clamped - newValue) > 0.0001 {
calculator.dailyUsageHours = clamped
}
autoUpdateSavedLoad()
}
}
private func normalizedVoltage(for value: Double) -> Double {
let rounded = roundToTenth(value)
if let snap = nearestValue(to: rounded, in: voltageSnapValues, tolerance: 0.3) {
@@ -951,6 +1320,14 @@ struct CalculatorView: View {
return rounded
}
private func clampDutyCyclePercent(_ value: Double) -> Double {
min(100, max(0, roundToTenth(value)))
}
private func clampUsageHours(_ value: Double) -> Double {
min(24, max(0, roundToTenth(value)))
}
private func nearestValue(to value: Double, in options: [Double], tolerance: Double) -> Double? {
guard let closest = options.min(by: { abs($0 - value) < abs($1 - value) }) else { return nil }
return abs(closest - value) <= tolerance ? closest : nil
@@ -999,6 +1376,16 @@ struct CalculatorView: View {
lengthInput = formattedValue(calculator.length)
editingValue = .length
}
private func beginDutyCycleEditing() {
dutyCycleInput = formattedValue(calculator.dutyCyclePercent)
editingValue = .dutyCycle
}
private func beginUsageHoursEditing() {
usageHoursInput = formattedValue(calculator.dailyUsageHours)
editingValue = .usageHours
}
private func saveCurrentLoad() {
@@ -1013,6 +1400,8 @@ struct CalculatorView: View {
iconName: "lightbulb",
colorName: "blue",
isWattMode: isWattMode,
dutyCyclePercent: calculator.dutyCyclePercent,
dailyUsageHours: calculator.dailyUsageHours,
system: nil, // For now, new loads aren't associated with a system
remoteIconURLString: nil
)
@@ -1025,6 +1414,8 @@ struct CalculatorView: View {
calculator.current = savedLoad.current
calculator.power = savedLoad.power
calculator.length = savedLoad.length
calculator.dutyCyclePercent = savedLoad.dutyCyclePercent
calculator.dailyUsageHours = savedLoad.dailyUsageHours
isWattMode = savedLoad.isWattMode
completedItemIDs = Set(savedLoad.bomCompletedItemIDs)
}
@@ -1041,6 +1432,8 @@ struct CalculatorView: View {
savedLoad.crossSection = calculator.crossSection(for: .metric)
savedLoad.timestamp = Date()
savedLoad.isWattMode = isWattMode
savedLoad.dutyCyclePercent = calculator.dutyCyclePercent
savedLoad.dailyUsageHours = calculator.dailyUsageHours
savedLoad.bomCompletedItemIDs = Array(completedItemIDs).sorted()
// Icon and color are updated directly through bindings in the editor
}
@@ -1196,8 +1589,19 @@ struct SliderSection: View {
var buttonAction: (() -> Void)?
var tapAction: (() -> Void)?
var snapValues: [Double]?
var isButtonVisible: Bool
init(title: String, value: Binding<Double>, range: ClosedRange<Double>, unit: String, buttonText: String? = nil, buttonAction: (() -> Void)? = nil, tapAction: (() -> Void)? = nil, snapValues: [Double]? = nil) {
init(
title: String,
value: Binding<Double>,
range: ClosedRange<Double>,
unit: String,
buttonText: String? = nil,
buttonAction: (() -> Void)? = nil,
tapAction: (() -> Void)? = nil,
snapValues: [Double]? = nil,
isButtonVisible: Bool = true
) {
self.title = title
self._value = value
self.range = range
@@ -1206,6 +1610,7 @@ struct SliderSection: View {
self.buttonAction = buttonAction
self.tapAction = tapAction
self.snapValues = snapValues
self.isButtonVisible = isButtonVisible
}
var body: some View {
@@ -1219,6 +1624,10 @@ struct SliderSection: View {
buttonAction?()
}
.buttonStyle(.borderedProminent)
.opacity(isButtonVisible ? 1 : 0)
.allowsHitTesting(isButtonVisible)
.accessibilityHidden(!isButtonVisible)
.disabled(!isButtonVisible || buttonAction == nil)
}
}
@@ -1264,6 +1673,97 @@ struct SliderSection: View {
}
}
struct EditableSliderRow: View {
struct AlertCopy {
let title: String
let placeholder: String
let message: String
let cancelTitle: String
let saveTitle: String
}
let title: String
let unit: String
let range: ClosedRange<Double>
@Binding var value: Double
var buttonText: String? = nil
var buttonAction: (() -> Void)? = nil
var isButtonVisible: Bool = true
var snapValues: [Double]? = nil
var sliderTransform: (Double) -> Double = { $0 }
var alertTransform: (Double) -> Double = { $0 }
var formatValue: (Double) -> String
var parseInput: (String) -> Double?
var alertCopy: AlertCopy
@State private var isPresentingAlert = false
@State private var inputText: String = ""
var body: some View {
SliderSection(
title: title,
value: Binding(
get: { value },
set: { newValue in
let transformed = sliderTransform(newValue)
let clamped = clampedValue(transformed)
value = clamped
}
),
range: range,
unit: unit,
buttonText: buttonText,
buttonAction: buttonAction,
tapAction: beginEditing,
snapValues: isPresentingAlert ? nil : snapValues,
isButtonVisible: isButtonVisible
)
.alert(
alertCopy.title,
isPresented: $isPresentingAlert
) {
TextField(alertCopy.placeholder, text: $inputText)
.keyboardType(.decimalPad)
.onAppear {
if inputText.isEmpty {
inputText = formatValue(value)
}
}
.onChange(of: inputText) { _, newValue in
guard let parsed = parseInput(newValue) else { return }
value = clampedAlertValue(parsed)
}
Button(alertCopy.cancelTitle, role: .cancel) {
inputText = ""
}
Button(alertCopy.saveTitle) {
if let parsed = parseInput(inputText) {
value = clampedAlertValue(parsed)
}
inputText = ""
}
} message: {
Text(alertCopy.message)
}
}
private func beginEditing() {
inputText = formatValue(value)
isPresentingAlert = true
}
private func clampedValue(_ newValue: Double) -> Double {
min(max(range.lowerBound, newValue), range.upperBound)
}
private func clampedAlertValue(_ rawValue: Double) -> Double {
let transformed = alertTransform(rawValue)
return clampedValue(transformed)
}
}
struct LoadLibraryView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@@ -1318,7 +1818,10 @@ struct LoadLibraryView: View {
calculator.loadName = savedLoad.name
calculator.voltage = savedLoad.voltage
calculator.current = savedLoad.current
calculator.power = savedLoad.power
calculator.length = savedLoad.length
calculator.dutyCyclePercent = savedLoad.dutyCyclePercent
calculator.dailyUsageHours = savedLoad.dailyUsageHours
}
private func deleteLoads(offsets: IndexSet) {

View File

@@ -301,45 +301,19 @@ struct SystemOverviewView: View {
} else {
ViewThatFits(in: .horizontal) {
HStack(spacing: 16) {
summaryMetric(
icon: "battery.100",
label: batteryCountLabel,
value: "\(batteries.count)",
tint: .blue
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "gauge.medium",
label: batteryCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "bolt.circle",
label: batteryEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
).frame(maxWidth: .infinity, alignment: .leading)
batteryMetricsContent
}
LazyVGrid(
columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: 2),
alignment: .leading,
spacing: 16
) {
batteryMetricsContent
}
VStack(alignment: .leading, spacing: 12) {
summaryMetric(
icon: "battery.100",
label: batteryCountLabel,
value: "\(batteries.count)",
tint: .blue
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "gauge.medium",
label: batteryCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "bolt.circle",
label: batteryEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
).frame(maxWidth: .infinity, alignment: .leading)
batteryMetricsContent
}
}
}
@@ -355,6 +329,34 @@ struct SystemOverviewView: View {
.buttonStyle(.plain)
}
@ViewBuilder
private var batteryMetricsContent: some View {
summaryMetric(
icon: "battery.100",
label: batteryCountLabel,
value: "\(batteries.count)",
tint: .blue
)
summaryMetric(
icon: "gauge.medium",
label: batteryCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
)
summaryMetric(
icon: "battery.100.bolt",
label: batteryUsableCapacityLabel,
value: formattedValue(totalUsableCapacity, unit: "Ah"),
tint: .teal
)
summaryMetric(
icon: "bolt.circle",
label: batteryUsableEnergyLabel,
value: formattedValue(totalUsableEnergy, unit: "Wh"),
tint: .green
)
}
private var chargersCard: some View {
Button(action: onSelectChargers) {
VStack(alignment: .leading, spacing: 16) {
@@ -467,6 +469,18 @@ struct SystemOverviewView: View {
}
}
private var totalUsableCapacity: Double {
batteries.reduce(0) { result, battery in
result + battery.usableCapacityAmpHours
}
}
private var totalUsableEnergy: Double {
batteries.reduce(0) { result, battery in
result + battery.usableEnergyWattHours
}
}
private var totalChargerCurrent: Double {
chargers.reduce(0) { result, charger in
result + max(0, charger.maxCurrentAmps)
@@ -611,8 +625,8 @@ struct SystemOverviewView: View {
}
private var estimatedRuntimeHours: Double? {
guard totalPower > 0, totalEnergy > 0 else { return nil }
let hours = totalEnergy / totalPower
guard totalPower > 0, totalUsableEnergy > 0 else { return nil }
let hours = totalUsableEnergy / totalPower
return hours.isFinite && hours > 0 ? hours : nil
}
@@ -738,12 +752,21 @@ struct SystemOverviewView: View {
)
}
private var batteryEnergyLabel: String {
private var batteryUsableCapacityLabel: String {
NSLocalizedString(
"battery.bank.metric.energy",
"battery.bank.metric.usable_capacity",
bundle: .main,
value: "Energy",
comment: "Label for total energy metric"
value: "Usable Capacity",
comment: "Label for usable capacity metric"
)
}
private var batteryUsableEnergyLabel: String {
NSLocalizedString(
"battery.bank.metric.usable_energy",
bundle: .main,
value: "Usable Energy",
comment: "Label for usable energy metric"
)
}

View File

@@ -0,0 +1,539 @@
import SwiftUI
import StoreKit
@MainActor
final class CableProPaywallViewModel: ObservableObject {
enum LoadingState: Equatable {
case idle
case loading
case loaded
case failed(String)
}
@Published private(set) var products: [Product] = []
@Published private(set) var state: LoadingState = .idle
@Published private(set) var purchasingProductID: String?
@Published private(set) var isRestoring = false
@Published private(set) var purchasedProductIDs: Set<String> = []
@Published var alert: PaywallAlert?
private let productIdentifiers: [String]
init(productIdentifiers: [String]) {
self.productIdentifiers = productIdentifiers
Task {
await updateCurrentEntitlements()
}
}
func loadProducts(force: Bool = false) async {
if state == .loading { return }
if !force, case .loaded = state { return }
guard !productIdentifiers.isEmpty else {
products = []
state = .loaded
return
}
state = .loading
do {
let fetched = try await Product.products(for: productIdentifiers)
products = fetched.sorted { productSortKey(lhs: $0, rhs: $1) }
state = .loaded
await updateCurrentEntitlements()
} catch {
state = .failed(error.localizedDescription)
}
}
private func productSortKey(lhs: Product, rhs: Product) -> Bool {
sortIndex(for: lhs) < sortIndex(for: rhs)
}
private func sortIndex(for product: Product) -> Int {
guard let period = product.subscription?.subscriptionPeriod else { return Int.max }
switch period.unit {
case .day: return 0
case .week: return 1
case .month: return 2
case .year: return 3
@unknown default: return 10
}
}
func purchase(_ product: Product) async {
guard purchasingProductID == nil else { return }
purchasingProductID = product.id
defer { purchasingProductID = nil }
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try verify(verification)
purchasedProductIDs.insert(transaction.productID)
alert = PaywallAlert(kind: .success, message: localizedString("cable.pro.alert.success.body", defaultValue: "Thanks for supporting Cable PRO!"))
await transaction.finish()
await updateCurrentEntitlements()
case .userCancelled:
break
case .pending:
alert = PaywallAlert(kind: .pending, message: localizedString("cable.pro.alert.pending.body", defaultValue: "Your purchase is awaiting approval."))
@unknown default:
alert = PaywallAlert(kind: .error, message: localizedString("cable.pro.alert.error.generic", defaultValue: "Something went wrong. Please try again."))
}
} catch {
alert = PaywallAlert(kind: .error, message: error.localizedDescription)
}
}
func restorePurchases() async {
guard !isRestoring else { return }
isRestoring = true
defer { isRestoring = false }
do {
try await AppStore.sync()
await updateCurrentEntitlements()
alert = PaywallAlert(kind: .restored, message: localizedString("cable.pro.alert.restored.body", defaultValue: "Your purchases are available again."))
} catch {
alert = PaywallAlert(kind: .error, message: error.localizedDescription)
}
}
private func verify<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let signed):
return signed
case .unverified(_, let error):
throw error
}
}
private func updateCurrentEntitlements() async {
var unlocked: Set<String> = []
for await result in Transaction.currentEntitlements {
switch result {
case .verified(let transaction):
unlocked.insert(transaction.productID)
case .unverified:
continue
}
}
purchasedProductIDs = unlocked
}
}
struct CableProPaywallView: View {
@Environment(\.dismiss) private var dismiss
@Binding var isPresented: Bool
@EnvironmentObject private var unitSettings: UnitSystemSettings
@StateObject private var viewModel: CableProPaywallViewModel
@State private var alertInfo: PaywallAlert?
private static let defaultProductIds = [
"app.voltplan.cable.weekly",
"app.voltplan.cable.yearly"
]
init(isPresented: Binding<Bool>, productIdentifiers: [String] = CableProPaywallView.defaultProductIds) {
_isPresented = isPresented
_viewModel = StateObject(wrappedValue: CableProPaywallViewModel(productIdentifiers: productIdentifiers))
}
var body: some View {
NavigationStack {
VStack(spacing: 24) {
header
featureList
plansSection
footer
}
.padding(.horizontal, 20)
.padding(.top, 28)
.padding(.bottom, 16)
.navigationTitle("Cable PRO")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Close") {
dismiss()
}
}
}
.task {
await viewModel.loadProducts(force: true)
unitSettings.isProUnlocked = !viewModel.purchasedProductIDs.isEmpty
}
.refreshable { await viewModel.loadProducts(force: true) }
}
.onChange(of: viewModel.alert) { newValue in
alertInfo = newValue
}
.alert(item: $alertInfo) { alert in
Alert(
title: Text(alert.title),
message: Text(alert.messageText),
dismissButton: .default(Text("OK")) {
viewModel.alert = nil
alertInfo = nil
}
)
}
.onChange(of: viewModel.purchasedProductIDs) { newValue in
unitSettings.isProUnlocked = !newValue.isEmpty
}
}
private var header: some View {
VStack(alignment: .leading, spacing: 12) {
Text(localizedString("cable.pro.paywall.title", defaultValue: "Unlock Cable PRO"))
.font(.largeTitle.bold())
Text(localizedString("cable.pro.paywall.subtitle", defaultValue: "Cable PRO enables more configuration options for loads, batteries and chargers."))
.font(.body)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var featureList: some View {
VStack(alignment: .leading, spacing: 10) {
paywallFeature(text: localizedString("cable.pro.feature.dutyCycle", defaultValue: "Duty-cycle aware cable calculators"), icon: "bolt.fill")
paywallFeature(text: localizedString("cable.pro.feature.batteryCapacity", defaultValue: "Configure usable battery capacity"), icon: "list.clipboard")
paywallFeature(text: localizedString("cable.pro.feature.usageBased", defaultValue: "Usage based calculations"), icon: "sparkles")
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private func paywallFeature(text: String, icon: String) -> some View {
HStack(spacing: 12) {
Image(systemName: icon)
.font(.headline)
.foregroundStyle(Color.accentColor)
.frame(width: 28, height: 28)
.background(
Circle()
.fill(Color.accentColor.opacity(0.12))
)
Text(text)
.font(.callout)
.foregroundStyle(.primary)
}
.padding(.vertical, 4)
}
@ViewBuilder
private var plansSection: some View {
switch viewModel.state {
case .idle, .loading:
RoundedRectangle(cornerRadius: 16)
.fill(Color(.secondarySystemBackground))
.frame(height: 140)
.overlay(ProgressView())
.frame(maxWidth: .infinity)
case .failed(let message):
VStack(spacing: 12) {
Text("We couldn't load Cable PRO at the moment.")
.font(.headline)
Text(message)
.font(.caption)
.foregroundStyle(.secondary)
Button(action: { Task { await viewModel.loadProducts(force: true) } }) {
Text("Try Again")
.font(.callout.weight(.semibold))
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
}
.buttonStyle(.borderedProminent)
}
.padding()
case .loaded:
if viewModel.products.isEmpty {
VStack(spacing: 12) {
Text("No plans are currently available.")
.font(.headline)
Text("Check back soon—Cable PRO launches in your region shortly.")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.secondarySystemBackground))
)
} else {
VStack(spacing: 12) {
let hasActiveSubscription = !viewModel.purchasedProductIDs.isEmpty
ForEach(viewModel.products) { product in
PlanCard(
product: product,
isProcessing: viewModel.purchasingProductID == product.id,
isPurchased: viewModel.purchasedProductIDs.contains(product.id),
isDisabled: hasActiveSubscription && !viewModel.purchasedProductIDs.contains(product.id)
) {
Task {
await viewModel.purchase(product)
}
}
}
}
}
}
}
private var footer: some View {
VStack(spacing: 12) {
Button {
Task {
await viewModel.restorePurchases()
}
} label: {
if viewModel.isRestoring {
ProgressView()
.frame(maxWidth: .infinity)
} else {
Text(localizedString("cable.pro.restore.button", defaultValue: "Restore Purchases"))
.font(.footnote.weight(.semibold))
}
}
.buttonStyle(.borderless)
.padding(.top, 8)
.disabled(viewModel.isRestoring)
HStack(spacing: 16) {
if let termsURL = localizedURL(forKey: "cable.pro.terms.url") {
Link(localizedString("cable.pro.terms.label", defaultValue: "Terms"), destination: termsURL)
}
if let privacyURL = localizedURL(forKey: "cable.pro.privacy.url") {
Link(localizedString("cable.pro.privacy.label", defaultValue: "Privacy"), destination: privacyURL)
}
}
.font(.footnote)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
}
private func localizedURL(forKey key: String) -> URL? {
let raw = localizedString(key, defaultValue: "")
guard let url = URL(string: raw), !raw.isEmpty else { return nil }
return url
}
}
private func localizedString(_ key: String, defaultValue: String) -> String {
NSLocalizedString(key, tableName: nil, bundle: .main, value: defaultValue, comment: "")
}
private func localizedDurationString(for period: Product.SubscriptionPeriod) -> String {
let locale = Locale.autoupdatingCurrent
let number = localizedNumber(period.value, locale: locale)
let unitBase: String
switch period.unit {
case .day: unitBase = "day"
case .week: unitBase = "week"
case .month: unitBase = "month"
case .year: unitBase = "year"
@unknown default: unitBase = "day"
}
if period.value == 1 {
let key = "cable.pro.duration.\(unitBase).singular"
return localizedString(key, defaultValue: singularDurationFallback(for: unitBase))
} else {
let key = "cable.pro.duration.\(unitBase).plural"
let template = localizedString(key, defaultValue: pluralDurationFallback(for: unitBase))
return String(format: template, number)
}
}
private func localizedNumber(_ value: Int, locale: Locale) -> String {
let formatter = NumberFormatter()
formatter.locale = locale
formatter.numberStyle = .decimal
return formatter.string(from: NSNumber(value: value)) ?? String(value)
}
private func singularDurationFallback(for unit: String) -> String {
switch unit {
case "day": return "every day"
case "week": return "every week"
case "month": return "every month"
case "year": return "every year"
default: return "every day"
}
}
private func pluralDurationFallback(for unit: String) -> String {
switch unit {
case "day": return "every %@ days"
case "week": return "every %@ weeks"
case "month": return "every %@ months"
case "year": return "every %@ years"
default: return "every %@ days"
}
}
private func trialDurationString(for period: Product.SubscriptionPeriod) -> String {
let locale = Locale.autoupdatingCurrent
let number = localizedNumber(period.value, locale: locale)
let unitBase: String
switch period.unit {
case .day: unitBase = "day"
case .week: unitBase = "week"
case .month: unitBase = "month"
case .year: unitBase = "year"
@unknown default: unitBase = "day"
}
let key = "cable.pro.trial.duration.\(unitBase).\(period.value == 1 ? "singular" : "plural")"
let fallbackTemplate: String
switch unitBase {
case "day": fallbackTemplate = "%@-day"
case "week": fallbackTemplate = "%@-week"
case "month": fallbackTemplate = "%@-month"
case "year": fallbackTemplate = "%@-year"
default: fallbackTemplate = "%@-day"
}
let template = localizedString(key, defaultValue: fallbackTemplate)
if template.contains("%@") {
return String(format: template, number)
} else {
return template
}
}
private struct PlanCard: View {
let product: Product
let isProcessing: Bool
let isPurchased: Bool
let isDisabled: Bool
let action: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .firstTextBaseline) {
Text(product.displayName)
.font(.headline)
Spacer()
Text(product.displayPrice)
.font(.headline)
}
if let info = product.subscription {
VStack(alignment: .leading, spacing: 6) {
if let trial = trialDescription(for: info) {
Text(trial)
.font(.caption.weight(.semibold))
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background(
Capsule()
.fill(Color.accentColor.opacity(0.15))
)
.foregroundStyle(Color.accentColor)
}
Text(subscriptionDescription(for: info))
.font(.caption)
.foregroundStyle(.secondary)
}
}
Button(action: action) {
Group {
if isProcessing {
ProgressView()
} else if isPurchased {
Label(localizedString("cable.pro.button.unlocked", defaultValue: "Unlocked"), systemImage: "checkmark.circle.fill")
.labelStyle(.titleAndIcon)
} else {
let titleKey = product.subscription?.introductoryOffer != nil ? "cable.pro.button.freeTrial" : "cable.pro.button.unlock"
Text(localizedString(titleKey, defaultValue: product.subscription?.introductoryOffer != nil ? "Start Free Trial" : "Unlock Now"))
}
}
.font(.callout.weight(.semibold))
.frame(maxWidth: .infinity)
.padding(.vertical, 6)
}
.buttonStyle(.borderedProminent)
.disabled(isProcessing || isPurchased || isDisabled)
.opacity((isPurchased || isDisabled) ? 0.6 : 1)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.secondarySystemBackground))
)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(isPurchased ? Color.accentColor : Color.clear, lineWidth: isPurchased ? 2 : 0)
)
}
private func trialDescription(for info: Product.SubscriptionInfo) -> String? {
guard
let offer = info.introductoryOffer,
offer.paymentMode == .freeTrial
else { return nil }
let duration = trialDurationString(for: offer.period)
let template = localizedString("cable.pro.trial.badge", defaultValue: "Includes a %@ free trial")
return String(format: template, duration)
}
private func subscriptionDescription(for info: Product.SubscriptionInfo) -> String {
let quantity = localizedDurationString(for: info.subscriptionPeriod)
let templateKey: String
if let offer = info.introductoryOffer,
offer.paymentMode == .freeTrial {
templateKey = "cable.pro.subscription.trialThenRenews"
} else {
templateKey = "cable.pro.subscription.renews"
}
let template = localizedString(templateKey, defaultValue: templateKey == "cable.pro.subscription.trialThenRenews" ? "Free trial, then renews every %@." : "Renews every %@.")
return String(format: template, quantity)
}
}
struct PaywallAlert: Identifiable, Equatable {
enum Kind { case success, pending, restored, error }
let id = UUID()
let kind: Kind
let message: String
var title: String {
switch kind {
case .success:
return localizedString("cable.pro.alert.success.title", defaultValue: "Cable PRO Unlocked")
case .pending:
return localizedString("cable.pro.alert.pending.title", defaultValue: "Purchase Pending")
case .restored:
return localizedString("cable.pro.alert.restored.title", defaultValue: "Purchases Restored")
case .error:
return localizedString("cable.pro.alert.error.title", defaultValue: "Purchase Failed")
}
}
var messageText: String {
message
}
}
#Preview {
CableProPaywallView(isPresented: .constant(true))
}

View File

@@ -7,6 +7,11 @@ class SavedBattery {
var name: String
var nominalVoltage: Double
var capacityAmpHours: Double
var usableCapacityOverrideFraction: Double?
var chargeVoltage: Double?
var cutOffVoltage: Double?
var minimumTemperatureCelsius: Double?
var maximumTemperatureCelsius: Double?
private var chemistryRawValue: String
var iconName: String = "battery.100"
var colorName: String = "blue"
@@ -19,6 +24,11 @@ class SavedBattery {
nominalVoltage: Double = 12.8,
capacityAmpHours: Double = 100,
chemistry: BatteryConfiguration.Chemistry = .lithiumIronPhosphate,
usableCapacityOverrideFraction: Double? = nil,
chargeVoltage: Double? = nil,
cutOffVoltage: Double? = nil,
minimumTemperatureCelsius: Double? = nil,
maximumTemperatureCelsius: Double? = nil,
iconName: String = "battery.100",
colorName: String = "blue",
system: ElectricalSystem? = nil,
@@ -28,6 +38,11 @@ class SavedBattery {
self.name = name
self.nominalVoltage = nominalVoltage
self.capacityAmpHours = capacityAmpHours
self.usableCapacityOverrideFraction = usableCapacityOverrideFraction
self.chargeVoltage = chargeVoltage
self.cutOffVoltage = cutOffVoltage
self.minimumTemperatureCelsius = minimumTemperatureCelsius
self.maximumTemperatureCelsius = maximumTemperatureCelsius
self.chemistryRawValue = chemistry.rawValue
self.iconName = iconName
self.colorName = colorName
@@ -47,4 +62,18 @@ class SavedBattery {
var energyWattHours: Double {
nominalVoltage * capacityAmpHours
}
var usableCapacityAmpHours: Double {
let fraction: Double
if let override = usableCapacityOverrideFraction {
fraction = max(0, min(1, override))
} else {
fraction = chemistry.usableCapacityFraction
}
return capacityAmpHours * fraction
}
var usableEnergyWattHours: Double {
usableCapacityAmpHours * nominalVoltage
}
}

View File

@@ -8,10 +8,16 @@
import SwiftUI
import SwiftData
import StoreKit
struct SettingsView: View {
@EnvironmentObject var unitSettings: UnitSystemSettings
@Environment(\.dismiss) private var dismiss
@Environment(\.openURL) private var openURL
@State private var showingProPaywall = false
@State private var isLoadingProStatus = true
@State private var proStatus: ProSubscriptionStatus?
var body: some View {
NavigationStack {
@@ -24,27 +30,9 @@ struct SettingsView: View {
}
.pickerStyle(.segmented)
}
Section {
HStack {
Text("Wire Cross-Section:")
Spacer()
Text(unitSettings.unitSystem.wireAreaUnit)
.foregroundColor(.secondary)
}
HStack {
Text("Length:")
Spacer()
Text(unitSettings.unitSystem.lengthUnit)
.foregroundColor(.secondary)
}
} header: {
Text("Current Units")
} footer: {
Text("Changing the unit system will apply to all calculations in the app.")
Section("Cable PRO") {
proSectionContent
}
Section {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 8) {
@@ -87,5 +75,172 @@ struct SettingsView: View {
}
}
}
.task { await loadProStatus() }
.sheet(isPresented: $showingProPaywall) {
CableProPaywallView(isPresented: $showingProPaywall)
}
.onChange(of: showingProPaywall) { isPresented in
if !isPresented {
Task { await loadProStatus() }
}
}
.onAppear {
Task { await loadProStatus() }
}
}
}
@ViewBuilder
private var proSectionContent: some View {
if isLoadingProStatus {
HStack {
Spacer()
ProgressView()
Spacer()
}
} else if let status = proStatus {
VStack(alignment: .leading, spacing: 8) {
Label(status.displayName, systemImage: "checkmark.seal.fill")
.font(.headline)
if let renewalDate = status.renewalDate {
Text(renewalText(for: renewalDate))
.font(.footnote)
.foregroundStyle(.secondary)
}
if let trialText = trialMessage(for: status) {
Text(trialText)
.font(.footnote)
.foregroundStyle(.secondary)
}
Text(localizedString("settings.pro.instructions", defaultValue: "Manage or cancel your subscription in the App Store."))
.font(.footnote)
.foregroundStyle(.secondary)
.padding(.top, 4)
Button {
openManageSubscriptions()
} label: {
Text(localizedString("settings.pro.manage.button", defaultValue: "Manage Subscription"))
.font(.callout.weight(.semibold))
.frame(maxWidth: .infinity)
.padding(6)
}
.buttonStyle(.borderedProminent)
}
.padding(.vertical, 4)
} else {
VStack(alignment: .leading, spacing: 10) {
Text(localizedString("settings.pro.cta.description", defaultValue: "Cable PRO keeps advanced calculations and early tools available."))
.font(.body)
.foregroundStyle(.secondary)
Button {
showingProPaywall = true
} label: {
Text(localizedString("settings.pro.cta.button", defaultValue: "Get Cable PRO"))
.font(.callout.weight(.semibold))
.frame(maxWidth: .infinity)
.padding(6)
}
.buttonStyle(.borderedProminent)
}
}
}
@MainActor
private func loadProStatus() async {
isLoadingProStatus = true
defer { isLoadingProStatus = false }
let status = await SettingsView.fetchProStatus()
proStatus = status
unitSettings.isProUnlocked = status != nil
}
private func renewalText(for date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
formatter.locale = Locale.autoupdatingCurrent
let dateString = formatter.string(from: date)
let template = localizedString("settings.pro.renewal.date", defaultValue: "Renews on %@.")
return String(format: template, dateString)
}
private func trialMessage(for status: ProSubscriptionStatus) -> String? {
guard status.isInTrial, let endDate = status.trialEndDate else { return nil }
let days = max(Calendar.autoupdatingCurrent.dateComponents([.day], from: Date(), to: endDate).day ?? 0, 0)
if days > 0 {
let dayText = localizedDayCount(days)
let template = localizedString("settings.pro.trial.remaining", defaultValue: "%@ remaining in free trial.")
return String(format: template, dayText)
} else {
return localizedString("settings.pro.trial.today", defaultValue: "Free trial renews today.")
}
}
private func localizedDayCount(_ days: Int) -> String {
let number = localizedNumber(days)
let key = days == 1 ? "settings.pro.day.one" : "settings.pro.day.other"
let template = localizedString(key, defaultValue: days == 1 ? "%@ day" : "%@ days")
return String(format: template, number)
}
private func openManageSubscriptions() {
guard let url = URL(string: localizedString("settings.pro.manage.url", defaultValue: "https://apps.apple.com/account/subscriptions")) else { return }
openURL(url)
}
private func localizedNumber(_ value: Int) -> String {
let formatter = NumberFormatter()
formatter.locale = Locale.autoupdatingCurrent
formatter.numberStyle = .decimal
return formatter.string(from: NSNumber(value: value)) ?? String(value)
}
static func fetchProStatus() async -> ProSubscriptionStatus? {
let productIDs = Set(["app.voltplan.cable.weekly", "app.voltplan.cable.yearly"])
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result,
productIDs.contains(transaction.productID) else { continue }
let product = try? await Product.products(for: [transaction.productID]).first
let displayName = product?.displayName ?? transaction.productID
let renewalDate = transaction.expirationDate
let hasIntroOffer = transaction.offerType == .introductory
let paymentMode = product?.subscription?.introductoryOffer?.paymentMode
let isInTrial = hasIntroOffer && paymentMode == .freeTrial
let trialEndDate = isInTrial ? transaction.expirationDate : nil
return ProSubscriptionStatus(
productId: transaction.productID,
displayName: displayName,
renewalDate: renewalDate,
isInTrial: isInTrial,
trialEndDate: trialEndDate
)
}
return nil
}
private func localizedString(_ key: String, defaultValue: String) -> String {
NSLocalizedString(key, tableName: nil, bundle: .main, value: defaultValue, comment: "")
}
struct ProSubscriptionStatus {
let productId: String
let displayName: String
let renewalDate: Date?
let isInTrial: Bool
let trialEndDate: Date?
}
}
#Preview("Settings (Default)") {
let settings = UnitSystemSettings()
return SettingsView()
.environmentObject(settings)
}

View File

@@ -153,6 +153,11 @@ struct SystemComponentsPersistence {
nominalVoltage: configuration.nominalVoltage,
capacityAmpHours: configuration.capacityAmpHours,
chemistry: configuration.chemistry,
usableCapacityOverrideFraction: configuration.usableCapacityOverrideFraction,
chargeVoltage: configuration.chargeVoltage,
cutOffVoltage: configuration.cutOffVoltage,
minimumTemperatureCelsius: configuration.minimumTemperatureCelsius,
maximumTemperatureCelsius: configuration.maximumTemperatureCelsius,
iconName: configuration.iconName,
colorName: configuration.colorName,
system: system

View File

@@ -46,9 +46,15 @@ class UnitSystemSettings: ObservableObject {
UserDefaults.standard.set(unitSystem.rawValue, forKey: "unitSystem")
}
}
@Published var isProUnlocked: Bool {
didSet {
UserDefaults.standard.set(isProUnlocked, forKey: "isProUnlocked")
}
}
init() {
let savedSystem = UserDefaults.standard.string(forKey: "unitSystem") ?? UnitSystem.metric.rawValue
self.unitSystem = UnitSystem(rawValue: savedSystem) ?? .metric
self.isProUnlocked = UserDefaults.standard.bool(forKey: "isProUnlocked")
}
}

View File

@@ -1,8 +1,138 @@
// Keys
"Add your first component" = "Erstelle deine erste Komponente";
"Bring your system to life with components and let **Cable by VoltPlan** handle cable and fuse recommendations." = "Verbraucher sind das Herz von Cable. Stelle sicher, dass du nie wieder ein zu dünnes Kabel oder die falsche Sicherung verwendest.";
"Browse" = "Durchsuchen";
"Browse Library" = "Bibliothek durchsuchen";
"Browse electrical components from VoltPlan" = "Elektrische Verbraucher von VoltPlan durchstöbern";
"Cancel" = "Abbrechen";
"Changing the unit system will apply to all calculations in the app." = "Die Änderung der Einheiten wirkt sich auf alle Berechnungen der App aus.";
"Check back soon for new loads from VoltPlan." = "Schau bald wieder vorbei, um neue Verbraucher von VoltPlan zu finden.";
"Close" = "Schließen";
"Color" = "Farbe";
"Coming soon - manage your electrical systems and panels here." = "Demnächst verfügbar verwalte hier deine elektrischen Systeme und Verteilungen.";
"Component Library" = "Komponentenbibliothek";
"Components" = "Verbraucher";
"Create Component" = "Komponente erstellen";
"Create System" = "System erstellen";
"Create your first system" = "Erstelle dein erstes System";
"Current" = "Strom";
"Current Units" = "Aktuelle Einheiten";
"Details" = "Details";
"Details coming soon" = "Details folgen in Kürze";
"Edit Current" = "Strom bearbeiten";
"Edit Length" = "Länge bearbeiten";
"Edit Power" = "Leistung bearbeiten";
"Edit Voltage" = "Spannung bearbeiten";
"Enter current in amperes (A)" = "Gib den Strom in Ampere (A) ein";
"Enter length in %@" = "Gib die Länge in %@ ein";
"Enter power in watts (W)" = "Gib die Leistung in Watt (W) ein";
"Enter voltage in volts (V)" = "Gib die Spannung in Volt (V) ein";
"FUSE" = "SICHERUNG";
"Give your setup a name so **Cable by VoltPlan** can organize loads, wiring, and recommendations in one place." = "Ob \"**Mein System**\", \"**Motoryacht LARGO**\" oder \"**Camper Luigi**\". Dein System braucht einen Namen.";
"Icon" = "Symbol";
"Important:" = "Wichtig:";
"Length" = "Länge";
"Length:" = "Länge:";
"Load Library" = "Verbraucher-bibliothek";
"Loading components" = "Komponenten werden geladen";
"New Load" = "Neuer Verbraucher";
"No components available" = "Keine Komponenten verfügbar";
"No loads saved in this system yet." = "Dieses System hat noch keine Verbraucher.";
"No matches" = "Keine Treffer";
"Power" = "Leistung";
"Preview" = "Vorschau";
"Retry" = "Erneut versuchen";
"Safety Disclaimer" = "Sicherheitshinweis";
"Save" = "Speichern";
"Search components" = "Komponenten suchen";
"Settings" = "Einstellungen";
"System" = "System";
"System Name" = "Systemname";
"System View" = "Systemansicht";
"Systems" = "Systeme";
"This application provides electrical calculations for educational and estimation purposes only." = "Diese Anwendung erstellt elektrische Berechnungen zu Schulungszwecken.";
"Try searching for a different name." = "Versuche, nach einem anderen Namen zu suchen.";
"Unable to load components" = "Komponenten konnten nicht geladen werden";
"Unit System" = "Einheitensystem";
"Units" = "Einheiten";
"VoltPlan Library" = "VoltPlan-Bibliothek";
"Voltage" = "Spannung";
"WIRE" = "KABEL";
"Wire Cross-Section:" = "Kabelquerschnitt:";
"affiliate.button.review_parts" = "Bauteile prüfen";
"affiliate.description.with_link" = "Tippen oben zeigt eine vollständige Stückliste, bevor der Affiliate-Link geöffnet wird. Käufe können VoltPlan unterstützen.";
"affiliate.description.without_link" = "Tippen oben zeigt eine vollständige Stückliste mit Einkaufssuchen, die dir bei der Beschaffung helfen.";
"affiliate.disclaimer" = "Käufe über Affiliate-Links können VoltPlan unterstützen.";
"battery.bank.badge.capacity" = "Kapazität";
"battery.bank.badge.energy" = "Energie";
"battery.bank.badge.voltage" = "Spannung";
"battery.bank.banner.capacity" = "Kapazitätsabweichung erkannt";
"battery.bank.banner.voltage" = "Spannungsabweichung erkannt";
"battery.bank.empty.subtitle" = "Tippe auf Plus, um eine Batterie für %@ zu konfigurieren.";
"battery.bank.empty.title" = "Noch keine Batterien";
"battery.bank.header.title" = "Batterien";
"battery.bank.metric.capacity" = "Kapazität";
"battery.bank.metric.count" = "Batterien";
"battery.bank.metric.energy" = "Energie";
"battery.bank.metric.usable_capacity" = "Nutzbare Kapazität";
"battery.bank.metric.usable_energy" = "Nutzbare Energie";
"battery.bank.status.capacity.message" = "%@ nutzt eine andere Kapazität als der dominante Bankwert %@. Unterschiedliche Kapazitäten verursachen ungleichmäßige Entladung und vorzeitige Alterung.";
"battery.bank.status.capacity.title" = "Kapazitätsabweichung";
"battery.bank.status.dismiss" = "Verstanden";
"battery.bank.status.multiple.batteries" = "%d Batterien";
"battery.bank.status.single.battery" = "Eine Batterie";
"battery.bank.status.voltage.message" = "%@ weicht vom Bank-Basiswert %@ ab. Unterschiedliche Nennspannungen führen zu ungleichmäßigem Laden und können Ladegeräte oder Wechselrichter beschädigen.";
"battery.bank.status.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_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.";
"battery.editor.advanced.temperature_range.helper" = "Definiere den empfohlenen Betriebstemperaturbereich.";
"battery.editor.alert.charge_voltage.message" = "Gib die Ladespannung in Volt (V) ein.";
"battery.editor.alert.charge_voltage.placeholder" = "Ladespannung";
"battery.editor.alert.charge_voltage.title" = "Ladespannung bearbeiten";
"battery.editor.alert.cutoff_voltage.message" = "Gib die Abschaltspannung in Volt (V) ein.";
"battery.editor.alert.cutoff_voltage.placeholder" = "Abschaltspannung";
"battery.editor.alert.cutoff_voltage.title" = "Abschaltspannung bearbeiten";
"battery.editor.alert.maximum_temperature.message" = "Gib die Höchsttemperatur in Grad Celsius (\u00B0C) ein.";
"battery.editor.alert.maximum_temperature.placeholder" = "Höchsttemperatur (\u00B0C)";
"battery.editor.alert.maximum_temperature.title" = "Höchsttemperatur bearbeiten";
"battery.editor.alert.minimum_temperature.message" = "Gib die Mindesttemperatur in Grad Celsius (\u00B0C) ein.";
"battery.editor.alert.minimum_temperature.placeholder" = "Mindesttemperatur (\u00B0C)";
"battery.editor.alert.minimum_temperature.title" = "Mindesttemperatur bearbeiten";
"battery.editor.alert.cancel" = "Abbrechen";
"battery.editor.alert.capacity.message" = "Kapazität in Amperestunden (Ah) eingeben";
"battery.editor.alert.capacity.placeholder" = "Kapazität";
"battery.editor.alert.capacity.title" = "Kapazität bearbeiten";
"battery.editor.alert.save" = "Speichern";
"battery.editor.alert.usable_capacity.message" = "Nutzbare Kapazität in Prozent (%) eingeben";
"battery.editor.alert.usable_capacity.placeholder" = "Nutzbare Kapazität (%)";
"battery.editor.alert.usable_capacity.title" = "Nutzbare Kapazität bearbeiten";
"battery.editor.alert.voltage.message" = "Spannung in Volt (V) eingeben";
"battery.editor.alert.voltage.placeholder" = "Spannung";
"battery.editor.alert.voltage.title" = "Nennspannung bearbeiten";
"battery.editor.button.reset_default" = "Zurücksetzen";
"battery.editor.cancel" = "Abbrechen";
"battery.editor.default_name" = "Neue Batterie";
"battery.editor.field.chemistry" = "Chemie";
"battery.editor.field.name" = "Name";
"battery.editor.placeholder.name" = "Hausbank";
"battery.editor.save" = "Speichern";
"battery.editor.section.advanced" = "Erweitert";
"battery.editor.section.summary" = "Übersicht";
"battery.editor.slider.capacity" = "Kapazität";
"battery.editor.slider.charge_voltage" = "Ladespannung";
"battery.editor.slider.cutoff_voltage" = "Abschaltspannung";
"battery.editor.slider.temperature_range" = "Temperaturbereich";
"battery.editor.slider.temperature_range.max" = "Maximum";
"battery.editor.slider.temperature_range.min" = "Minimum";
"battery.editor.slider.usable_capacity" = "Nutzbare Kapazität (%)";
"battery.editor.slider.voltage" = "Nennspannung";
"battery.editor.title" = "Batterie einrichten";
"battery.onboarding.subtitle" = "Behalte Kapazität und Chemie deiner Batterien im Blick, um die Laufzeit im Griff zu behalten.";
"battery.onboarding.title" = "Füge deine erste Batterie hinzu";
"battery.overview.empty.create" = "Batterie hinzufügen";
"bom.accessibility.mark.complete" = "Markiere %@ als erledigt";
"bom.accessibility.mark.incomplete" = "Markiere %@ als unerledigt";
"bom.fuse.detail" = "Inline-Halter und %dA-Sicherung";
@@ -14,11 +144,66 @@
"bom.navigation.title.system" = "Stückliste %@";
"bom.size.unknown" = "Größe offen";
"bom.terminals.detail" = "Ring- oder Gabelkabelschuhe für %@-Leitungen";
"cable.pro.privacy.label" = "Datenschutz";
"cable.pro.privacy.url" = "https://voltplan.app/de/datenschutz";
"cable.pro.terms.label" = "Nutzungsbedingungen";
"cable.pro.terms.url" = "https://voltplan.app/de/agb";
"calculator.advanced.duty_cycle.helper" = "Prozentsatz der aktiven Zeit, in der die Last tatsächlich Leistung aufnimmt.";
"calculator.advanced.duty_cycle.title" = "Einschaltdauer";
"calculator.advanced.section.title" = "Erweitert";
"calculator.advanced.usage_hours.helper" = "Stunden pro Tag, in denen die Last eingeschaltet ist.";
"calculator.advanced.usage_hours.title" = "Tägliche Laufzeit";
"calculator.advanced.usage_hours.unit" = "h/Tag";
"calculator.alert.duty_cycle.message" = "Einschaltdauer als Prozent (0-100 %) eingeben.";
"calculator.alert.duty_cycle.placeholder" = "Einschaltdauer";
"calculator.alert.duty_cycle.title" = "Einschaltdauer bearbeiten";
"calculator.alert.usage_hours.message" = "Stunden pro Tag eingeben, in denen die Last aktiv ist.";
"calculator.alert.usage_hours.placeholder" = "Tägliche Laufzeit";
"calculator.alert.usage_hours.title" = "Tägliche Laufzeit bearbeiten";
"charger.default.new" = "Neues Ladegerät";
"charger.editor.alert.cancel" = "Abbrechen";
"charger.editor.alert.current.message" = "Strom in Ampere (A) eingeben";
"charger.editor.alert.current.title" = "Ladestrom bearbeiten";
"charger.editor.alert.input_voltage.title" = "Eingangsspannung bearbeiten";
"charger.editor.alert.output_voltage.title" = "Ausgangsspannung bearbeiten";
"charger.editor.alert.power.message" = "Leistung in Watt (W) eingeben";
"charger.editor.alert.power.placeholder" = "Leistung";
"charger.editor.alert.power.title" = "Ladeleistung bearbeiten";
"charger.editor.alert.save" = "Speichern";
"charger.editor.alert.voltage.message" = "Spannung in Volt (V) eingeben";
"charger.editor.appearance.accessibility" = "Darstellung des Ladegeräts bearbeiten";
"charger.editor.appearance.subtitle" = "Passe an, wie dieses Ladegerät angezeigt wird";
"charger.editor.appearance.title" = "Ladegerät-Darstellung";
"charger.editor.default_name" = "Neues Ladegerät";
"charger.editor.field.current" = "Ladestrom";
"charger.editor.field.input_voltage" = "Eingangsspannung";
"charger.editor.field.name" = "Name";
"charger.editor.field.output_voltage" = "Ausgangsspannung";
"charger.editor.field.power" = "Ladeleistung";
"charger.editor.field.power.footer" = "Leer lassen, wenn keine Leistungsangabe vorliegt. Wir berechnen sie aus Spannung und Strom.";
"charger.editor.placeholder.name" = "Werkstattladegerät";
"charger.editor.section.electrical" = "Elektrik";
"charger.editor.section.power" = "Ladeausgang";
"charger.editor.title" = "Ladegerät";
"chargers.badge.current" = "Strom";
"chargers.badge.input" = "Eingang";
"chargers.badge.output" = "Ausgang";
"chargers.badge.power" = "Leistung";
"chargers.onboarding.primary" = "Ladegerät erstellen";
"chargers.onboarding.subtitle" = "Verwalte Landstrom, Booster und Solarregler, um deine Ladeleistung im Blick zu behalten.";
"chargers.onboarding.title" = "Füge deine Ladegeräte hinzu";
"chargers.subtitle" = "Ladegeräte-Komponenten sind bald verfügbar.";
"chargers.summary.metric.count" = "Ladegeräte";
"chargers.summary.metric.current" = "Ladestrom";
"chargers.summary.metric.output" = "Spannung";
"chargers.summary.metric.power" = "Ladeleistung";
"chargers.summary.title" = "Ladeübersicht";
"chargers.title" = "Ladegeräte für %@";
"component.fallback.name" = "Komponente";
"default.load.library" = "Bibliothekslast";
"default.load.name" = "Mein Verbraucher";
"default.load.unnamed" = "Unbenannter Verbraucher";
"default.load.new" = "Neuer Verbraucher";
"default.load.unnamed" = "Unbenannter Verbraucher";
"default.system.name" = "Mein System";
"default.system.new" = "Neues System";
"editor.load.name_field" = "Name des Verbrauchers";
@@ -27,234 +212,134 @@
"editor.system.location.optional" = "Standort (optional)";
"editor.system.name_field" = "Name des Systems";
"editor.system.title" = "System bearbeiten";
"loads.library.button" = "Bibliothek";
"loads.metric.cable" = "Schnitt";
"loads.metric.fuse" = "Sicherung";
"loads.metric.length" = "Länge";
"loads.onboarding.subtitle" = "Statte dein System mit Verbrauchern aus und lass **Cable by VoltPlan** die Kabel- und Sicherungsempfehlungen übernehmen.";
"loads.onboarding.title" = "Füge deinen ersten Verbraucher hinzu";
"loads.overview.empty.create" = "Verbraucher hinzufügen";
"loads.overview.empty.library" = "Bibliothek durchsuchen";
"loads.overview.empty.message" = "Füge einen Verbraucher hinzu, um dein System einzurichten.";
"loads.overview.header.title" = "Verbraucher";
"loads.overview.metric.count" = "Verbraucher";
"loads.overview.metric.current" = "Strom";
"loads.overview.metric.power" = "Leistung";
"loads.overview.status.missing_details.banner" = "Konfiguration deiner Verbraucher abschließen";
"loads.overview.status.missing_details.message" = "Gib Kabellänge und Leitungsquerschnitt für %d %@ ein, um genaue Empfehlungen zu erhalten.";
"loads.overview.status.missing_details.plural" = "Verbraucher";
"loads.overview.status.missing_details.singular" = "Verbraucher";
"loads.overview.status.missing_details.title" = "Fehlende Verbraucherdetails";
"overview.chargers.empty.create" = "Ladegerät hinzufügen";
"overview.chargers.empty.subtitle" = "Füge Landstrom-, DC-DC- oder Solarladegeräte hinzu, um deine Ladeleistung zu verstehen.";
"overview.chargers.empty.title" = "Noch keine Ladegeräte konfiguriert";
"overview.chargers.header.title" = "Ladegeräte";
"overview.loads.empty.subtitle" = "Füge Verbraucher hinzu, um auf dieses System zugeschnittene Kabel- und Sicherungsempfehlungen zu erhalten.";
"overview.loads.empty.title" = "Noch keine Verbraucher eingerichtet";
"overview.runtime.subtitle" = "Bei dauerhafter Vollast";
"overview.runtime.title" = "Geschätzte Laufzeit";
"overview.runtime.unavailable" = "Trage Batteriekapazität und Leistungsaufnahme ein, um die Laufzeit zu schätzen.";
"overview.system.header.title" = "Systemübersicht";
"sample.charger.dcdc.name" = "DC-DC-Ladegerät";
"sample.charger.shore.name" = "Landstrom-Ladegerät";
"sample.charger.workbench.name" = "Werkbank-Ladegerät";
"sample.load.charger.name" = "Werkzeugladegerät";
"sample.load.compressor.name" = "Luftkompressor";
"sample.load.fridge.name" = "Kompressor-Kühlschrank";
"sample.load.lighting.name" = "LED-Streifenbeleuchtung";
"sample.system.rv.location" = "12V Wohnstromkreis";
"sample.system.rv.name" = "Abenteuer-Van";
"sample.system.workshop.location" = "Werkzeugecke";
"sample.system.workshop.name" = "Werkbank";
"slider.button.ampere" = "Ampere";
"slider.button.watt" = "Watt";
"slider.current.title" = "Strom";
"slider.length.title" = "Kabellänge (%@)";
"slider.power.title" = "Leistung";
"slider.voltage.title" = "Spannung";
"system.list.no.components" = "Noch keine Verbraucher";
"units.imperial.display" = "Imperial (AWG, ft)";
"units.metric.display" = "Metrisch (mm², m)";
"sample.system.rv.name" = "Abenteuer-Van";
"sample.system.rv.location" = "12V Wohnstromkreis";
"sample.system.workshop.name" = "Werkbank";
"sample.system.workshop.location" = "Werkzeugecke";
"sample.load.fridge.name" = "Kompressor-Kühlschrank";
"sample.load.lighting.name" = "LED-Streifenbeleuchtung";
"sample.load.compressor.name" = "Luftkompressor";
"sample.load.charger.name" = "Werkzeugladegerät";
"system.icon.keywords.rv" = "wohnmobil, camper, campervan, van, reisemobil, campingbus";
"system.icon.keywords.truck" = "lkw, anhänger, zugmaschine, trailer";
"system.icon.keywords.boat" = "boot, schiff, yacht, segel, segelboot";
"system.icon.keywords.plane" = "flugzeug, flug, flieger, luft";
"system.icon.keywords.ferry" = "fähre, schiff";
"system.icon.keywords.house" = "haus, zuhause, hütte, ferienhaus, lodge";
"system.icon.keywords.building" = "gebäude, büro, lagerhalle, fabrik, anlage";
"system.icon.keywords.tent" = "camp, camping, zelt, outdoor";
"system.icon.keywords.solar" = "solar, sonne, pv";
"system.icon.keywords.battery" = "batterie, speicher, akku";
"system.icon.keywords.server" = "server, daten, netzwerk, rack, rechenzentrum";
"system.icon.keywords.computer" = "computer, elektronik, labor, technik";
"system.icon.keywords.gear" = "getriebe, mechanik, maschine, werkstatt";
"system.icon.keywords.tool" = "werkzeug, wartung, reparatur, werkstatt";
"system.icon.keywords.hammer" = "hammer, tischlerei, zimmerei";
"system.icon.keywords.light" = "licht, beleuchtung, lampe";
"system.icon.keywords.boat" = "boot, schiff, yacht, segel, segelboot";
"system.icon.keywords.bolt" = "strom, power, elektrisch, spannung";
"system.icon.keywords.plug" = "stecker, netzstecker";
"system.icon.keywords.engine" = "motor, generator, antrieb";
"system.icon.keywords.fuel" = "kraftstoff, diesel, benzin";
"system.icon.keywords.water" = "wasser, pumpe, tank";
"system.icon.keywords.heat" = "heizung, heizer, ofen";
"system.icon.keywords.cold" = "kalt, kühlen, eis, gefrieren";
"system.icon.keywords.building" = "gebäude, büro, lagerhalle, fabrik, anlage";
"system.icon.keywords.climate" = "klima, hvac, temperatur";
// Direct strings
"Systems" = "Systeme";
"System" = "System";
"System View" = "Systemansicht";
"System Name" = "Systemname";
"Create System" = "System erstellen";
"Create your first system" = "Erstelle dein erstes System";
"Give your setup a name so **Cable by VoltPlan** can organize loads, wiring, and recommendations in one place." = "Ob \"**Mein System**\", \"**Motoryacht LARGO**\" oder \"**Camper Luigi**\". Dein System braucht einen Namen.";
"Add your first component" = "Erstelle deine erste Komponente";
"Bring your system to life with components and let **Cable by VoltPlan** handle cable and fuse recommendations." = "Verbraucher sind das Herz von Cable. Stelle sicher, dass du nie wieder ein zu dünnes Kabel oder die falsche Sicherung verwendest.";
"Create Component" = "Komponente erstellen";
"Browse Library" = "Bibliothek durchsuchen";
"Browse" = "Durchsuchen";
"Browse electrical components from VoltPlan" = "Elektrische Verbraucher von VoltPlan durchstöbern";
"Component Library" = "Komponentenbibliothek";
"Details coming soon" = "Details folgen in Kürze";
"Components" = "Verbraucher";
"FUSE" = "SICHERUNG";
"WIRE" = "KABEL";
"Current" = "Strom";
"Power" = "Leistung";
"Voltage" = "Spannung";
"Length" = "Länge";
"Length:" = "Länge:";
"Wire Cross-Section:" = "Kabelquerschnitt:";
"Current Units" = "Aktuelle Einheiten";
"Changing the unit system will apply to all calculations in the app." = "Die Änderung der Einheiten wirkt sich auf alle Berechnungen der App aus.";
"Unit System" = "Einheitensystem";
"Units" = "Einheiten";
"Settings" = "Einstellungen";
"Close" = "Schließen";
"Cancel" = "Abbrechen";
"Save" = "Speichern";
"Retry" = "Erneut versuchen";
"Loading components" = "Komponenten werden geladen";
"Unable to load components" = "Komponenten konnten nicht geladen werden";
"No components available" = "Keine Komponenten verfügbar";
"No matches" = "Keine Treffer";
"Check back soon for new loads from VoltPlan." = "Schau bald wieder vorbei, um neue Verbraucher von VoltPlan zu finden.";
"Try searching for a different name." = "Versuche, nach einem anderen Namen zu suchen.";
"Search components" = "Komponenten suchen";
"No loads saved in this system yet." = "Dieses System hat noch keine Verbraucher.";
"Coming soon - manage your electrical systems and panels here." = "Demnächst verfügbar verwalte hier deine elektrischen Systeme und Verteilungen.";
"Load Library" = "Verbraucher-bibliothek";
"Safety Disclaimer" = "Sicherheitshinweis";
"This application provides electrical calculations for educational and estimation purposes only." = "Diese Anwendung erstellt elektrische Berechnungen zu Schulungszwecken.";
"Important:" = "Wichtig:";
"• Always consult qualified electricians for actual installations" = "• Ziehe für tatsächliche Installationen stets qualifizierte Elektriker hinzu";
"• Follow all local electrical codes and regulations" = "• Beachte alle örtlichen Vorschriften und Normen";
"• Electrical work should only be performed by licensed professionals" = "• Elektroarbeiten sollten nur von zertifizierten Fachkräften ausgeführt werden";
"• These calculations may not account for all environmental factors" = "• Diese Berechnungen berücksichtigen möglicherweise nicht alle Umgebungsfaktoren";
"• The app developers assume no liability for electrical installations" = "• Die App-Entwickler übernehmen keine Haftung für elektrische Installationen";
"Enter length in %@" = "Gib die Länge in %@ ein";
"Enter voltage in volts (V)" = "Gib die Spannung in Volt (V) ein";
"Enter current in amperes (A)" = "Gib den Strom in Ampere (A) ein";
"Enter power in watts (W)" = "Gib die Leistung in Watt (W) ein";
"Edit Length" = "Länge bearbeiten";
"Edit Voltage" = "Spannung bearbeiten";
"Edit Current" = "Strom bearbeiten";
"Edit Power" = "Leistung bearbeiten";
"Preview" = "Vorschau";
"Details" = "Details";
"Icon" = "Symbol";
"Color" = "Farbe";
"VoltPlan Library" = "VoltPlan-Bibliothek";
"New Load" = "Neuer Verbraucher";
"tab.overview" = "Übersicht";
"tab.components" = "Verbraucher";
"system.icon.keywords.cold" = "kalt, kühlen, eis, gefrieren";
"system.icon.keywords.computer" = "computer, elektronik, labor, technik";
"system.icon.keywords.engine" = "motor, generator, antrieb";
"system.icon.keywords.ferry" = "fähre, schiff";
"system.icon.keywords.fuel" = "kraftstoff, diesel, benzin";
"system.icon.keywords.gear" = "getriebe, mechanik, maschine, werkstatt";
"system.icon.keywords.hammer" = "hammer, tischlerei, zimmerei";
"system.icon.keywords.heat" = "heizung, heizer, ofen";
"system.icon.keywords.house" = "haus, zuhause, hütte, ferienhaus, lodge";
"system.icon.keywords.light" = "licht, beleuchtung, lampe";
"system.icon.keywords.plane" = "flugzeug, flug, flieger, luft";
"system.icon.keywords.plug" = "stecker, netzstecker";
"system.icon.keywords.rv" = "wohnmobil, camper, campervan, van, reisemobil, campingbus";
"system.icon.keywords.server" = "server, daten, netzwerk, rack, rechenzentrum";
"system.icon.keywords.solar" = "solar, sonne, pv";
"system.icon.keywords.tent" = "camp, camping, zelt, outdoor";
"system.icon.keywords.tool" = "werkzeug, wartung, reparatur, werkstatt";
"system.icon.keywords.truck" = "lkw, anhänger, zugmaschine, trailer";
"system.icon.keywords.water" = "wasser, pumpe, tank";
"system.list.no.components" = "Noch keine Verbraucher";
"tab.batteries" = "Batterien";
"tab.chargers" = "Ladegeräte";
"loads.overview.header.title" = "Verbraucher";
"loads.overview.metric.count" = "Verbraucher";
"loads.overview.metric.current" = "Strom";
"loads.overview.metric.power" = "Leistung";
"loads.overview.empty.message" = "Füge einen Verbraucher hinzu, um dein System einzurichten.";
"loads.overview.empty.create" = "Verbraucher hinzufügen";
"loads.overview.empty.library" = "Bibliothek durchsuchen";
"loads.library.button" = "Bibliothek";
"loads.onboarding.title" = "Füge deinen ersten Verbraucher hinzu";
"loads.onboarding.subtitle" = "Statte dein System mit Verbrauchern aus und lass **Cable by VoltPlan** die Kabel- und Sicherungsempfehlungen übernehmen.";
"loads.overview.status.missing_details.title" = "Fehlende Verbraucherdetails";
"loads.overview.status.missing_details.message" = "Gib Kabellänge und Leitungsquerschnitt für %d %@ ein, um genaue Empfehlungen zu erhalten.";
"loads.overview.status.missing_details.singular" = "Verbraucher";
"loads.overview.status.missing_details.plural" = "Verbraucher";
"loads.overview.status.missing_details.banner" = "Konfiguration deiner Verbraucher abschließen";
"loads.metric.fuse" = "Sicherung";
"loads.metric.cable" = "Schnitt";
"loads.metric.length" = "Länge";
"overview.system.header.title" = "Systemübersicht";
"overview.loads.empty.title" = "Noch keine Verbraucher eingerichtet";
"overview.loads.empty.subtitle" = "Füge Verbraucher hinzu, um auf dieses System zugeschnittene Kabel- und Sicherungsempfehlungen zu erhalten.";
"overview.runtime.title" = "Geschätzte Laufzeit";
"overview.runtime.subtitle" = "Bei dauerhafter Vollast";
"overview.runtime.unavailable" = "Trage Batteriekapazität und Leistungsaufnahme ein, um die Laufzeit zu schätzen.";
"battery.bank.warning.voltage.short" = "Spannung";
"battery.bank.warning.capacity.short" = "Kapazität";
"battery.bank.header.title" = "Batterien";
"battery.bank.metric.count" = "Batterien";
"battery.bank.metric.capacity" = "Kapazität";
"battery.bank.metric.energy" = "Energie";
"battery.overview.empty.create" = "Batterie hinzufügen";
"battery.onboarding.title" = "Füge deine erste Batterie hinzu";
"battery.onboarding.subtitle" = "Behalte Kapazität und Chemie deiner Batterien im Blick, um die Laufzeit im Griff zu behalten.";
"battery.bank.badge.voltage" = "Spannung";
"overview.chargers.header.title" = "Ladegeräte";
"overview.chargers.empty.title" = "Noch keine Ladegeräte konfiguriert";
"overview.chargers.empty.subtitle" = "Füge Landstrom-, DC-DC- oder Solarladegeräte hinzu, um deine Ladeleistung zu verstehen.";
"overview.chargers.empty.create" = "Ladegerät hinzufügen";
"battery.bank.badge.capacity" = "Kapazität";
"battery.bank.badge.energy" = "Energie";
"battery.bank.banner.voltage" = "Spannungsabweichung erkannt";
"battery.bank.banner.capacity" = "Kapazitätsabweichung erkannt";
"battery.bank.empty.title" = "Noch keine Batterien";
"battery.bank.empty.subtitle" = "Tippe auf Plus, um eine Batterie für %@ zu konfigurieren.";
"battery.bank.status.dismiss" = "Verstanden";
"battery.bank.status.single.battery" = "Eine Batterie";
"battery.bank.status.multiple.batteries" = "%d Batterien";
"battery.bank.status.voltage.title" = "Spannungsabweichung";
"battery.bank.status.voltage.message" = "%@ weicht vom Bank-Basiswert %@ ab. Unterschiedliche Nennspannungen führen zu ungleichmäßigem Laden und können Ladegeräte oder Wechselrichter beschädigen.";
"battery.bank.status.capacity.title" = "Kapazitätsabweichung";
"battery.bank.status.capacity.message" = "%@ nutzt eine andere Kapazität als der dominante Bankwert %@. Unterschiedliche Kapazitäten verursachen ungleichmäßige Entladung und vorzeitige Alterung.";
"battery.editor.title" = "Batterie einrichten";
"battery.editor.cancel" = "Abbrechen";
"battery.editor.save" = "Speichern";
"battery.editor.field.name" = "Name";
"battery.editor.placeholder.name" = "Hausbank";
"battery.editor.field.chemistry" = "Chemie";
"battery.editor.section.summary" = "Übersicht";
"battery.editor.slider.voltage" = "Nennspannung";
"battery.editor.slider.capacity" = "Kapazität";
"battery.editor.alert.voltage.title" = "Nennspannung bearbeiten";
"battery.editor.alert.voltage.placeholder" = "Spannung";
"battery.editor.alert.voltage.message" = "Spannung in Volt (V) eingeben";
"battery.editor.alert.capacity.title" = "Kapazität bearbeiten";
"battery.editor.alert.capacity.placeholder" = "Kapazität";
"battery.editor.alert.capacity.message" = "Kapazität in Amperestunden (Ah) eingeben";
"battery.editor.alert.cancel" = "Abbrechen";
"battery.editor.alert.save" = "Speichern";
"battery.editor.default_name" = "Neue Batterie";
"charger.editor.title" = "Ladegerät";
"charger.editor.field.name" = "Name";
"charger.editor.placeholder.name" = "Werkstattladegerät";
"charger.editor.section.electrical" = "Elektrik";
"charger.editor.section.power" = "Ladeausgang";
"charger.editor.appearance.title" = "Ladegerät-Darstellung";
"charger.editor.appearance.subtitle" = "Passe an, wie dieses Ladegerät angezeigt wird";
"charger.editor.appearance.accessibility" = "Darstellung des Ladegeräts bearbeiten";
"charger.editor.field.input_voltage" = "Eingangsspannung";
"charger.editor.field.output_voltage" = "Ausgangsspannung";
"charger.editor.field.current" = "Ladestrom";
"charger.editor.field.power" = "Ladeleistung";
"charger.editor.field.power.footer" = "Leer lassen, wenn keine Leistungsangabe vorliegt. Wir berechnen sie aus Spannung und Strom.";
"charger.editor.default_name" = "Neues Ladegerät";
"charger.editor.alert.input_voltage.title" = "Eingangsspannung bearbeiten";
"charger.editor.alert.output_voltage.title" = "Ausgangsspannung bearbeiten";
"charger.editor.alert.current.title" = "Ladestrom bearbeiten";
"charger.editor.alert.voltage.message" = "Spannung in Volt (V) eingeben";
"charger.editor.alert.power.title" = "Ladeleistung bearbeiten";
"charger.editor.alert.power.placeholder" = "Leistung";
"charger.editor.alert.power.message" = "Leistung in Watt (W) eingeben";
"charger.editor.alert.current.message" = "Strom in Ampere (A) eingeben";
"charger.editor.alert.cancel" = "Abbrechen";
"charger.editor.alert.save" = "Speichern";
"charger.default.new" = "Neues Ladegerät";
"chargers.summary.title" = "Ladeübersicht";
"chargers.summary.metric.count" = "Ladegeräte";
"chargers.summary.metric.output" = "Spannung";
"chargers.summary.metric.current" = "Ladestrom";
"chargers.summary.metric.power" = "Ladeleistung";
"chargers.badge.input" = "Eingang";
"chargers.badge.output" = "Ausgang";
"chargers.badge.current" = "Strom";
"chargers.badge.power" = "Leistung";
"chargers.onboarding.title" = "Füge deine Ladegeräte hinzu";
"chargers.onboarding.subtitle" = "Verwalte Landstrom, Booster und Solarregler, um deine Ladeleistung im Blick zu behalten.";
"chargers.onboarding.primary" = "Ladegerät erstellen";
"sample.charger.shore.name" = "Landstrom-Ladegerät";
"sample.charger.dcdc.name" = "DC-DC-Ladegerät";
"sample.charger.workbench.name" = "Werkbank-Ladegerät";
"chargers.title" = "Ladegeräte für %@";
"chargers.subtitle" = "Ladegeräte-Komponenten sind bald verfügbar.";
"tab.components" = "Verbraucher";
"tab.overview" = "Übersicht";
"units.imperial.display" = "Imperial (AWG, ft)";
"units.metric.display" = "Metrisch (mm², m)";
"settings.pro.cta.description" = "Cable PRO ermöglicht detailliertere Einstellungen für Verbraucher, Batterien und Ladegeräte.";
"settings.pro.cta.button" = "Cable PRO abonnieren";
"settings.pro.renewal.date" = "Nächste Verlängerung am %@.";
"settings.pro.trial.remaining" = "%@ verbleibend in der Testphase.";
"settings.pro.trial.today" = "Die Testphase endet heute.";
"settings.pro.instructions" = "Verwalte oder kündige dein Abonnement im App Store.";
"settings.pro.manage.button" = "Abonnement verwalten";
"settings.pro.manage.url" = "https://apps.apple.com/account/subscriptions";
"settings.pro.day.one" = "%@ Tag";
"settings.pro.day.other" = "%@ Tage";
"cable.pro.terms.label" = "AGB";
"cable.pro.privacy.label" = "Datenschutz";
"cable.pro.terms.url" = "https://voltplan.app/terms";
"cable.pro.privacy.url" = "https://voltplan.app/privacy";
"cable.pro.paywall.title" = "Cable PRO";
"cable.pro.paywall.subtitle" = "Cable PRO bietet mehr Konfigurationsoptionen für Verbraucher, Batterien und Ladegeräte.";
"cable.pro.feature.dutyCycle" = "Kabelberechnungen mit Einschaltdauer";
"cable.pro.feature.batteryCapacity" = "Verfügbare Batteriekapazität konfigurieren";
"cable.pro.feature.usageBased" = "Nutzungsbasierte Berechnungen";
"cable.pro.button.unlock" = "Jetzt freischalten";
"cable.pro.button.freeTrial" = "Kostenlose Testphase starten";
"cable.pro.button.unlocked" = "Bereits aktiviert";
"cable.pro.restore.button" = "Käufe wiederherstellen";
"cable.pro.alert.success.title" = "Cable PRO aktiviert";
"cable.pro.alert.success.body" = "Danke für deine Unterstützung!";
"cable.pro.alert.pending.title" = "Kauf ausstehend";
"cable.pro.alert.pending.body" = "Dein Kauf wartet auf Bestätigung.";
"cable.pro.alert.restored.title" = "Käufe wiederhergestellt";
"cable.pro.alert.restored.body" = "Deine bisherigen Käufe sind wieder verfügbar.";
"cable.pro.alert.error.title" = "Kauf fehlgeschlagen";
"cable.pro.alert.error.generic" = "Etwas ist schiefgelaufen. Bitte versuche es erneut.";
"cable.pro.trial.badge" = "Enthält eine %@ Testphase";
"cable.pro.subscription.renews" = "Verlängert sich %@.";
"cable.pro.subscription.trialThenRenews" = "Testphase, danach Verlängerung %@.";
"cable.pro.duration.day.singular" = "jeden Tag";
"cable.pro.duration.day.plural" = "alle %@ Tage";
"cable.pro.duration.week.singular" = "jede Woche";
"cable.pro.duration.week.plural" = "alle %@ Wochen";
"cable.pro.duration.month.singular" = "monatlich";
"cable.pro.duration.month.plural" = "alle %@ Monate";
"cable.pro.duration.year.singular" = "jährlich";
"cable.pro.duration.year.plural" = "alle %@ Jahre";
"cable.pro.trial.duration.day.singular" = "%@-tägige";
"cable.pro.trial.duration.day.plural" = "%@-tägige";
"cable.pro.trial.duration.week.singular" = "%@-wöchige";
"cable.pro.trial.duration.week.plural" = "%@-wöchige";
"cable.pro.trial.duration.month.singular" = "%@-monatige";
"cable.pro.trial.duration.month.plural" = "%@-monatige";
"cable.pro.trial.duration.year.singular" = "%@-jährige";
"cable.pro.trial.duration.year.plural" = "%@-jährige";
"• Always consult qualified electricians for actual installations" = "• Ziehe für tatsächliche Installationen stets qualifizierte Elektriker hinzu";
"• Electrical work should only be performed by licensed professionals" = "• Elektroarbeiten sollten nur von zertifizierten Fachkräften ausgeführt werden";
"• Follow all local electrical codes and regulations" = "• Beachte alle örtlichen Vorschriften und Normen";
"• The app developers assume no liability for electrical installations" = "• Die App-Entwickler übernehmen keine Haftung für elektrische Installationen";
"• These calculations may not account for all environmental factors" = "• Diese Berechnungen berücksichtigen möglicherweise nicht alle Umgebungsfaktoren";

View File

@@ -33,6 +33,18 @@
"slider.length.title" = "Longitud del cable (%@)";
"slider.power.title" = "Potencia";
"slider.voltage.title" = "Voltaje";
"calculator.advanced.section.title" = "Configuración avanzada";
"calculator.advanced.duty_cycle.title" = "Ciclo de trabajo";
"calculator.advanced.duty_cycle.helper" = "Porcentaje del tiempo activo en el que la carga consume energía.";
"calculator.advanced.usage_hours.title" = "Tiempo encendido diario";
"calculator.advanced.usage_hours.helper" = "Horas por día que la carga permanece encendida.";
"calculator.advanced.usage_hours.unit" = "h/día";
"calculator.alert.duty_cycle.title" = "Editar ciclo de trabajo";
"calculator.alert.duty_cycle.placeholder" = "Ciclo de trabajo";
"calculator.alert.duty_cycle.message" = "Introduce el porcentaje de ciclo de trabajo (0-100%).";
"calculator.alert.usage_hours.title" = "Editar tiempo encendido diario";
"calculator.alert.usage_hours.placeholder" = "Tiempo encendido diario";
"calculator.alert.usage_hours.message" = "Introduce las horas por día que la carga está activa.";
"system.list.no.components" = "Aún no hay componentes";
"units.imperial.display" = "Imperial (AWG, ft)";
"units.metric.display" = "Métrico (mm², m)";
@@ -171,6 +183,8 @@
"battery.bank.metric.count" = "Baterías";
"battery.bank.metric.capacity" = "Capacidad";
"battery.bank.metric.energy" = "Energía";
"battery.bank.metric.usable_capacity" = "Capacidad utilizable";
"battery.bank.metric.usable_energy" = "Energía utilizable";
"battery.overview.empty.create" = "Añadir batería";
"battery.onboarding.title" = "Añade tu primera batería";
"battery.onboarding.subtitle" = "Controla la capacidad y la química del banco para mantener tus tiempos de autonomía bajo control.";
@@ -202,12 +216,40 @@
"battery.editor.section.summary" = "Resumen";
"battery.editor.slider.voltage" = "Voltaje nominal";
"battery.editor.slider.capacity" = "Capacidad";
"battery.editor.slider.usable_capacity" = "Capacidad utilizable (%)";
"battery.editor.slider.charge_voltage" = "Voltaje de carga";
"battery.editor.slider.cutoff_voltage" = "Voltaje de corte";
"battery.editor.slider.temperature_range" = "Rango de temperatura";
"battery.editor.slider.temperature_range.min" = "Mínimo";
"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_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.";
"battery.editor.advanced.temperature_range.helper" = "Define el rango de temperatura de operación recomendado.";
"battery.editor.alert.voltage.title" = "Editar voltaje nominal";
"battery.editor.alert.voltage.placeholder" = "Voltaje";
"battery.editor.alert.voltage.message" = "Introduce el voltaje en voltios (V)";
"battery.editor.alert.capacity.title" = "Editar capacidad";
"battery.editor.alert.capacity.placeholder" = "Capacidad";
"battery.editor.alert.capacity.message" = "Introduce la capacidad en amperios-hora (Ah)";
"battery.editor.alert.usable_capacity.title" = "Editar capacidad utilizable";
"battery.editor.alert.usable_capacity.placeholder" = "Capacidad utilizable (%)";
"battery.editor.alert.usable_capacity.message" = "Introduce el porcentaje de capacidad utilizable (%)";
"battery.editor.alert.charge_voltage.title" = "Editar voltaje de carga";
"battery.editor.alert.charge_voltage.placeholder" = "Voltaje de carga";
"battery.editor.alert.charge_voltage.message" = "Introduce el voltaje de carga en voltios (V).";
"battery.editor.alert.cutoff_voltage.title" = "Editar voltaje de corte";
"battery.editor.alert.cutoff_voltage.placeholder" = "Voltaje de corte";
"battery.editor.alert.cutoff_voltage.message" = "Introduce el voltaje de corte en voltios (V).";
"battery.editor.alert.minimum_temperature.title" = "Editar temperatura mínima";
"battery.editor.alert.minimum_temperature.placeholder" = "Temperatura mínima (\u00B0C)";
"battery.editor.alert.minimum_temperature.message" = "Introduce la temperatura mínima en grados Celsius (\u00B0C).";
"battery.editor.alert.maximum_temperature.title" = "Editar temperatura máxima";
"battery.editor.alert.maximum_temperature.placeholder" = "Temperatura máxima (\u00B0C)";
"battery.editor.alert.maximum_temperature.message" = "Introduce la temperatura máxima en grados Celsius (\u00B0C).";
"battery.editor.alert.cancel" = "Cancelar";
"battery.editor.alert.save" = "Guardar";
"battery.editor.default_name" = "Nueva batería";
@@ -257,3 +299,9 @@
"chargers.title" = "Cargadores para %@";
"chargers.subtitle" = "Los componentes de carga estarán disponibles pronto.";
"cable.pro.paywall.title" = "Cable PRO";
"cable.pro.paywall.subtitle" = "Cable PRO permite más opciones de configuración para cargas, baterías y cargadores.";
"cable.pro.feature.dutyCycle" = "Calculadoras de cables conscientes del ciclo de trabajo";
"cable.pro.feature.batteryCapacity" = "Configura la capacidad utilizable de la batería";
"cable.pro.feature.usageBased" = "Cálculos basados en el uso";

View File

@@ -33,6 +33,18 @@
"slider.length.title" = "Longueur du câble (%@)";
"slider.power.title" = "Puissance";
"slider.voltage.title" = "Tension";
"calculator.advanced.section.title" = "Paramètres avancés";
"calculator.advanced.duty_cycle.title" = "Facteur de marche";
"calculator.advanced.duty_cycle.helper" = "Pourcentage du temps actif pendant lequel la charge consomme réellement de l'énergie.";
"calculator.advanced.usage_hours.title" = "Temps de fonctionnement quotidien";
"calculator.advanced.usage_hours.helper" = "Heures par jour pendant lesquelles la charge est allumée.";
"calculator.advanced.usage_hours.unit" = "h/jour";
"calculator.alert.duty_cycle.title" = "Modifier le facteur de marche";
"calculator.alert.duty_cycle.placeholder" = "Facteur de marche";
"calculator.alert.duty_cycle.message" = "Saisissez le facteur de marche en pourcentage (0-100 %).";
"calculator.alert.usage_hours.title" = "Modifier le temps de fonctionnement quotidien";
"calculator.alert.usage_hours.placeholder" = "Temps de fonctionnement quotidien";
"calculator.alert.usage_hours.message" = "Saisissez le nombre d'heures par jour pendant lesquelles la charge est active.";
"system.list.no.components" = "Aucun composant pour l'instant";
"units.imperial.display" = "Impérial (AWG, ft)";
"units.metric.display" = "Métrique (mm², m)";
@@ -171,6 +183,8 @@
"battery.bank.metric.count" = "Batteries";
"battery.bank.metric.capacity" = "Capacité";
"battery.bank.metric.energy" = "Énergie";
"battery.bank.metric.usable_capacity" = "Capacité utilisable";
"battery.bank.metric.usable_energy" = "Énergie utilisable";
"battery.overview.empty.create" = "Ajouter une batterie";
"battery.onboarding.title" = "Ajoutez votre première batterie";
"battery.onboarding.subtitle" = "Suivez la capacité et la chimie de votre banc pour mieux maîtriser l'autonomie.";
@@ -202,12 +216,40 @@
"battery.editor.section.summary" = "Résumé";
"battery.editor.slider.voltage" = "Tension nominale";
"battery.editor.slider.capacity" = "Capacité";
"battery.editor.slider.usable_capacity" = "Capacité utilisable (%)";
"battery.editor.slider.charge_voltage" = "Tension de charge";
"battery.editor.slider.cutoff_voltage" = "Tension de coupure";
"battery.editor.slider.temperature_range" = "Plage de température";
"battery.editor.slider.temperature_range.min" = "Minimum";
"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_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.";
"battery.editor.advanced.temperature_range.helper" = "Définissez la plage de température de fonctionnement recommandée.";
"battery.editor.alert.voltage.title" = "Modifier la tension nominale";
"battery.editor.alert.voltage.placeholder" = "Tension";
"battery.editor.alert.voltage.message" = "Saisissez la tension en volts (V)";
"battery.editor.alert.capacity.title" = "Modifier la capacité";
"battery.editor.alert.capacity.placeholder" = "Capacité";
"battery.editor.alert.capacity.message" = "Saisissez la capacité en ampères-heures (Ah)";
"battery.editor.alert.usable_capacity.title" = "Modifier la capacité utilisable";
"battery.editor.alert.usable_capacity.placeholder" = "Capacité utilisable (%)";
"battery.editor.alert.usable_capacity.message" = "Saisissez le pourcentage de capacité utilisable (%)";
"battery.editor.alert.charge_voltage.title" = "Modifier la tension de charge";
"battery.editor.alert.charge_voltage.placeholder" = "Tension de charge";
"battery.editor.alert.charge_voltage.message" = "Saisissez la tension de charge en volts (V).";
"battery.editor.alert.cutoff_voltage.title" = "Modifier la tension de coupure";
"battery.editor.alert.cutoff_voltage.placeholder" = "Tension de coupure";
"battery.editor.alert.cutoff_voltage.message" = "Saisissez la tension de coupure en volts (V).";
"battery.editor.alert.minimum_temperature.title" = "Modifier la température minimale";
"battery.editor.alert.minimum_temperature.placeholder" = "Température minimale (\u00B0C)";
"battery.editor.alert.minimum_temperature.message" = "Saisissez la température minimale en degrés Celsius (\u00B0C).";
"battery.editor.alert.maximum_temperature.title" = "Modifier la température maximale";
"battery.editor.alert.maximum_temperature.placeholder" = "Température maximale (\u00B0C)";
"battery.editor.alert.maximum_temperature.message" = "Saisissez la température maximale en degrés Celsius (\u00B0C).";
"battery.editor.alert.cancel" = "Annuler";
"battery.editor.alert.save" = "Enregistrer";
"battery.editor.default_name" = "Nouvelle batterie";
@@ -257,3 +299,9 @@
"chargers.title" = "Chargeurs pour %@";
"chargers.subtitle" = "Les chargeurs seront bientôt disponibles ici.";
"cable.pro.paywall.title" = "Cable PRO";
"cable.pro.paywall.subtitle" = "Cable PRO offre davantage d'options de configuration pour les charges, les batteries et les chargeurs.";
"cable.pro.feature.dutyCycle" = "Calculs de câbles tenant compte du cycle d'utilisation";
"cable.pro.feature.batteryCapacity" = "Configurez la capacité utilisable de la batterie";
"cable.pro.feature.usageBased" = "Calculs basés sur l'utilisation";

View File

@@ -33,6 +33,18 @@
"slider.length.title" = "Kabellengte (%@)";
"slider.power.title" = "Vermogen";
"slider.voltage.title" = "Spanning";
"calculator.advanced.section.title" = "Geavanceerde instellingen";
"calculator.advanced.duty_cycle.title" = "Inschakelduur";
"calculator.advanced.duty_cycle.helper" = "Percentage van de actieve tijd waarin de belasting daadwerkelijk vermogen vraagt.";
"calculator.advanced.usage_hours.title" = "Dagelijkse aan-tijd";
"calculator.advanced.usage_hours.helper" = "Uren per dag dat de belasting is ingeschakeld.";
"calculator.advanced.usage_hours.unit" = "u/dag";
"calculator.alert.duty_cycle.title" = "Inschakelduur bewerken";
"calculator.alert.duty_cycle.placeholder" = "Inschakelduur";
"calculator.alert.duty_cycle.message" = "Voer de inschakelduur in als percentage (0-100%).";
"calculator.alert.usage_hours.title" = "Dagelijkse aan-tijd bewerken";
"calculator.alert.usage_hours.placeholder" = "Dagelijkse aan-tijd";
"calculator.alert.usage_hours.message" = "Voer het aantal uren per dag in dat de belasting actief is.";
"system.list.no.components" = "Nog geen componenten";
"units.imperial.display" = "Imperiaal (AWG, ft)";
"units.metric.display" = "Metrisch (mm², m)";
@@ -171,6 +183,8 @@
"battery.bank.metric.count" = "Batterijen";
"battery.bank.metric.capacity" = "Capaciteit";
"battery.bank.metric.energy" = "Energie";
"battery.bank.metric.usable_capacity" = "Beschikbare capaciteit";
"battery.bank.metric.usable_energy" = "Beschikbare energie";
"battery.overview.empty.create" = "Accu toevoegen";
"battery.onboarding.title" = "Voeg je eerste accu toe";
"battery.onboarding.subtitle" = "Houd capaciteit en chemie van de accubank in de gaten om de gebruiksduur te beheersen.";
@@ -202,12 +216,40 @@
"battery.editor.section.summary" = "Overzicht";
"battery.editor.slider.voltage" = "Nominale spanning";
"battery.editor.slider.capacity" = "Capaciteit";
"battery.editor.slider.usable_capacity" = "Beschikbare capaciteit (%)";
"battery.editor.slider.charge_voltage" = "Laadspanning";
"battery.editor.slider.cutoff_voltage" = "Afsluitspanning";
"battery.editor.slider.temperature_range" = "Temperatuurbereik";
"battery.editor.slider.temperature_range.min" = "Minimum";
"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.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.";
"battery.editor.alert.voltage.title" = "Nominale spanning bewerken";
"battery.editor.alert.voltage.placeholder" = "Spanning";
"battery.editor.alert.voltage.message" = "Voer de spanning in volt (V) in";
"battery.editor.alert.capacity.title" = "Capaciteit bewerken";
"battery.editor.alert.capacity.placeholder" = "Capaciteit";
"battery.editor.alert.capacity.message" = "Voer de capaciteit in ampère-uur (Ah) in";
"battery.editor.alert.usable_capacity.title" = "Beschikbare capaciteit bewerken";
"battery.editor.alert.usable_capacity.placeholder" = "Beschikbare capaciteit (%)";
"battery.editor.alert.usable_capacity.message" = "Voer het percentage beschikbare capaciteit (%) in";
"battery.editor.alert.charge_voltage.title" = "Laadspanning bewerken";
"battery.editor.alert.charge_voltage.placeholder" = "Laadspanning";
"battery.editor.alert.charge_voltage.message" = "Voer de laadspanning in volt (V) in.";
"battery.editor.alert.cutoff_voltage.title" = "Afsluitspanning bewerken";
"battery.editor.alert.cutoff_voltage.placeholder" = "Afsluitspanning";
"battery.editor.alert.cutoff_voltage.message" = "Voer de afsluitspanning in volt (V) in.";
"battery.editor.alert.minimum_temperature.title" = "Minimale temperatuur bewerken";
"battery.editor.alert.minimum_temperature.placeholder" = "Minimale temperatuur (\u00B0C)";
"battery.editor.alert.minimum_temperature.message" = "Voer de minimale temperatuur in graden Celsius (\u00B0C) in.";
"battery.editor.alert.maximum_temperature.title" = "Maximale temperatuur bewerken";
"battery.editor.alert.maximum_temperature.placeholder" = "Maximale temperatuur (\u00B0C)";
"battery.editor.alert.maximum_temperature.message" = "Voer de maximale temperatuur in graden Celsius (\u00B0C) in.";
"battery.editor.alert.cancel" = "Annuleren";
"battery.editor.alert.save" = "Opslaan";
"battery.editor.default_name" = "Nieuwe batterij";
@@ -257,3 +299,9 @@
"chargers.title" = "Laders voor %@";
"chargers.subtitle" = "Ladercomponenten zijn binnenkort beschikbaar.";
"cable.pro.paywall.title" = "Cable PRO";
"cable.pro.paywall.subtitle" = "Cable PRO biedt meer configuratie-opties voor verbruikers, batterijen en laders.";
"cable.pro.feature.dutyCycle" = "Kabelberekeningen die rekening houden met de inschakelduur";
"cable.pro.feature.batteryCapacity" = "Configureer bruikbare batterijcapaciteit";
"cable.pro.feature.usageBased" = "Gebruiksgestuurde berekeningen";