added subscription booking
This commit is contained in:
@@ -2,6 +2,57 @@
|
||||
"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.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.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 +64,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,189 +132,124 @@
|
||||
"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";
|
||||
"calculator.advanced.section.title" = "Advanced Settings";
|
||||
"calculator.advanced.duty_cycle.title" = "Duty Cycle";
|
||||
"calculator.advanced.duty_cycle.helper" = "Percentage of each active session where the load actually draws power.";
|
||||
"calculator.advanced.usage_hours.title" = "Daily On-Time";
|
||||
"calculator.advanced.usage_hours.helper" = "Hours per day the load is turned on.";
|
||||
"calculator.advanced.usage_hours.unit" = "h/day";
|
||||
"calculator.alert.duty_cycle.title" = "Edit Duty Cycle";
|
||||
"calculator.alert.duty_cycle.placeholder" = "Duty Cycle";
|
||||
"calculator.alert.duty_cycle.message" = "Enter duty cycle as a percentage (0-100%).";
|
||||
"calculator.alert.usage_hours.title" = "Edit Daily On-Time";
|
||||
"calculator.alert.usage_hours.placeholder" = "Daily On-Time";
|
||||
"calculator.alert.usage_hours.message" = "Enter the number of hours per day the load is active.";
|
||||
"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.bank.metric.usable_capacity" = "Usable Capacity";
|
||||
"battery.bank.metric.usable_energy" = "Usable 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.slider.usable_capacity" = "Usable Capacity (%)";
|
||||
"battery.editor.section.advanced" = "Advanced";
|
||||
"battery.editor.button.reset_default" = "Reset";
|
||||
"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.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.usable_capacity.title" = "Edit Usable Capacity";
|
||||
"battery.editor.alert.usable_capacity.placeholder" = "Usable Capacity (%)";
|
||||
"battery.editor.alert.usable_capacity.message" = "Enter usable capacity percentage (%)";
|
||||
"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.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";
|
||||
|
||||
@@ -36,6 +36,10 @@ struct BatteryConfiguration: Identifiable, Hashable {
|
||||
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
|
||||
@@ -48,6 +52,10 @@ struct BatteryConfiguration: Identifiable, Hashable {
|
||||
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
|
||||
@@ -57,6 +65,10 @@ struct BatteryConfiguration: Identifiable, Hashable {
|
||||
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
|
||||
@@ -69,6 +81,16 @@ struct BatteryConfiguration: Identifiable, Hashable {
|
||||
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
|
||||
@@ -107,6 +129,10 @@ struct BatteryConfiguration: Identifiable, Hashable {
|
||||
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
|
||||
@@ -122,6 +148,10 @@ extension BatteryConfiguration {
|
||||
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
|
||||
@@ -133,6 +163,10 @@ extension BatteryConfiguration {
|
||||
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
@@ -25,8 +25,11 @@ struct CalculatorView: View {
|
||||
@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?
|
||||
|
||||
@@ -76,6 +79,9 @@ struct CalculatorView: View {
|
||||
navigationWrapped(mainLayout)
|
||||
)
|
||||
)
|
||||
.task {
|
||||
hasActiveProSubscription = (await SettingsView.fetchProStatus()) != nil
|
||||
}
|
||||
}
|
||||
|
||||
private func attachAlerts<V: View>(_ view: V) -> some View {
|
||||
@@ -345,6 +351,9 @@ struct CalculatorView: 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)
|
||||
@@ -980,6 +989,10 @@ struct CalculatorView: View {
|
||||
)
|
||||
}
|
||||
|
||||
private var advancedFeaturesEnabled: Bool {
|
||||
unitSettings.isProUnlocked || hasActiveProSubscription
|
||||
}
|
||||
|
||||
private var slidersSection: some View {
|
||||
Section {
|
||||
voltageSlider
|
||||
@@ -995,26 +1008,75 @@ struct CalculatorView: View {
|
||||
}
|
||||
|
||||
private var advancedSettingsSection: some View {
|
||||
Section(
|
||||
header: Text(advancedSettingsTitle.uppercased())
|
||||
.font(.caption2)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary),
|
||||
footer: VStack(alignment: .leading, spacing: 8) {
|
||||
Text(dutyCycleHelperText)
|
||||
Text(usageHoursHelperText)
|
||||
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))
|
||||
}
|
||||
dutyCycleSlider
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color(.systemBackground))
|
||||
.opacity(advancedEnabled ? 1 : 0.35)
|
||||
.allowsHitTesting(advancedEnabled)
|
||||
usageHoursSlider
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color(.systemBackground))
|
||||
.opacity(advancedEnabled ? 1 : 0.35)
|
||||
.allowsHitTesting(advancedEnabled)
|
||||
}
|
||||
} footer: {
|
||||
if isAdvancedExpanded {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(dutyCycleHelperText)
|
||||
Text(usageHoursHelperText)
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(advancedEnabled ? 1 : 0.35)
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
) {
|
||||
dutyCycleSlider
|
||||
.listRowSeparator(.hidden)
|
||||
usageHoursSlider
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
.listRowBackground(Color(.systemBackground))
|
||||
.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> {
|
||||
@@ -1524,8 +1586,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
|
||||
@@ -1534,6 +1607,7 @@ struct SliderSection: View {
|
||||
self.buttonAction = buttonAction
|
||||
self.tapAction = tapAction
|
||||
self.snapValues = snapValues
|
||||
self.isButtonVisible = isButtonVisible
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -1547,6 +1621,10 @@ struct SliderSection: View {
|
||||
buttonAction?()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.opacity(isButtonVisible ? 1 : 0)
|
||||
.allowsHitTesting(isButtonVisible)
|
||||
.accessibilityHidden(!isButtonVisible)
|
||||
.disabled(!isButtonVisible || buttonAction == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1592,6 +1670,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
|
||||
|
||||
539
Cable/Paywall/CableProPaywallView.swift
Normal file
539
Cable/Paywall/CableProPaywallView.swift
Normal 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("Unlock Cable PRO")
|
||||
.font(.largeTitle.bold())
|
||||
Text("Keep advanced calculations available and get every new tool the moment it ships.")
|
||||
.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: "Duty-cycle aware cable calculators", icon: "bolt.fill")
|
||||
paywallFeature(text: "Full BOM exports & sourcing help", icon: "list.clipboard")
|
||||
paywallFeature(text: "Early access to new tools & units", 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))
|
||||
}
|
||||
@@ -8,6 +8,10 @@ class SavedBattery {
|
||||
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"
|
||||
@@ -21,6 +25,10 @@ class SavedBattery {
|
||||
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,
|
||||
@@ -31,6 +39,10 @@ class SavedBattery {
|
||||
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
|
||||
|
||||
@@ -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,18 +30,8 @@ struct SettingsView: View {
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
|
||||
|
||||
Section("Cable Pro") {
|
||||
Toggle(isOn: $unitSettings.isProUnlocked) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Early Access Features")
|
||||
.font(.body.weight(.semibold))
|
||||
Text("Enable experimental tools that will require a paid upgrade later on.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
proSectionContent
|
||||
}
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
@@ -79,6 +75,167 @@ 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?
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +1,118 @@
|
||||
// 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.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.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 +124,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,256 +192,129 @@
|
||||
"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";
|
||||
"calculator.advanced.section.title" = "Erweitert";
|
||||
"calculator.advanced.duty_cycle.title" = "Einschaltdauer";
|
||||
"calculator.advanced.duty_cycle.helper" = "Prozentsatz der aktiven Zeit, in der die Last tatsächlich Leistung aufnimmt.";
|
||||
"calculator.advanced.usage_hours.title" = "Tägliche Laufzeit";
|
||||
"calculator.advanced.usage_hours.helper" = "Stunden pro Tag, in denen die Last eingeschaltet ist.";
|
||||
"calculator.advanced.usage_hours.unit" = "h/Tag";
|
||||
"calculator.alert.duty_cycle.title" = "Einschaltdauer bearbeiten";
|
||||
"calculator.alert.duty_cycle.placeholder" = "Einschaltdauer";
|
||||
"calculator.alert.duty_cycle.message" = "Einschaltdauer als Prozent (0-100 %) eingeben.";
|
||||
"calculator.alert.usage_hours.title" = "Tägliche Laufzeit bearbeiten";
|
||||
"calculator.alert.usage_hours.placeholder" = "Tägliche Laufzeit";
|
||||
"calculator.alert.usage_hours.message" = "Stunden pro Tag eingeben, in denen die Last aktiv ist.";
|
||||
"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.bank.metric.usable_capacity" = "Nutzbare Kapazität";
|
||||
"battery.bank.metric.usable_energy" = "Nutzbare 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.slider.usable_capacity" = "Nutzbare Kapazität (%)";
|
||||
"battery.editor.section.advanced" = "Erweitert";
|
||||
"battery.editor.button.reset_default" = "Zurücksetzen";
|
||||
"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.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.usable_capacity.title" = "Nutzbare Kapazität bearbeiten";
|
||||
"battery.editor.alert.usable_capacity.placeholder" = "Nutzbare Kapazität (%)";
|
||||
"battery.editor.alert.usable_capacity.message" = "Nutzbare Kapazität in Prozent (%) 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 hält erweiterte Berechnungen und neue Werkzeuge verfügbar.";
|
||||
"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.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";
|
||||
|
||||
Reference in New Issue
Block a user