Add analytics tracking, overview PDF export, and charger updates

Add first launch event, share tracking for overview PDF and diagram
exports. Add SystemOverviewPDFExporter for A4 PDF generation. Update
charger model with new configuration fields and localization for all
5 languages. Refresh app icon assets and CLAUDE.md build instructions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Lange-Hegermann
2026-03-25 17:45:54 +01:00
parent 5a5e8b8fbe
commit 03878b9507
19 changed files with 1698 additions and 28 deletions

View File

@@ -4,21 +4,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Build & Test Commands
```bash
# Build
xcodebuild -scheme Cable -destination 'platform=iOS Simulator,name=iPhone 15' build
Use the **Xcode MCP server** tools instead of `xcodebuild` CLI:
# Run all tests (unit + UI)
xcodebuild -scheme Cable -destination 'platform=iOS Simulator,name=iPhone 15' test
- **List windows first**: Always run `mcp__xcode__XcodeListWindows` to find the correct `tabIdentifier` — it changes depending on which Xcode windows are open.
- **Build**: `mcp__xcode__BuildProject`
- **Run all tests**: `mcp__xcode__RunAllTests`
- **Check errors**: `mcp__xcode__GetBuildLog` (severity: `error`) or `mcp__xcode__XcodeListNavigatorIssues`
# Run only unit tests
xcodebuild -scheme Cable -destination 'platform=iOS Simulator,name=iPhone 15' -only-testing:CableTests test
# Open in Xcode (preferred for UI iteration with previews)
open Cable.xcodeproj
```
Requires Xcode 15+ and iOS 17 simulator. No external dependencies beyond the Xcode toolchain.
No external dependencies beyond the Xcode toolchain.
## Architecture
@@ -56,7 +49,17 @@ Each feature has its own directory under `Cable/`: `Loads/`, `Systems/`, `Batter
## Localization
5 languages: English (base), German, Spanish, French, Dutch. UI strings use `String(localized:)`. Translation files are in `*.lproj/Localizable.strings` and `Localizable.stringsdict`.
5 languages: English (base), German, Spanish, French, Dutch. Translation files are in `*.lproj/Localizable.strings` and `Localizable.stringsdict`.
- Use `String(localized:defaultValue:)`**not** `NSLocalizedString`. The `defaultValue` serves as English fallback and avoids showing raw keys when a Localizable.strings entry is missing.
- When adding new user-facing strings, add translations to **all 5** Localizable.strings files immediately.
## PDF Export Pattern
PDF exports use `UIGraphicsPDFRenderer` with A4 portrait format. The pattern is:
- **Exporter struct** (e.g. `SystemOverviewPDFExporter`, `SystemBillOfMaterialsPDFExporter`) with snapshot data types — keeps Core Graphics rendering isolated from SwiftUI/SwiftData.
- **ShareSheet** triggered via `@State` item binding in the parent view.
- **Toolbar button** (not inline content) for the export action.
## StoreKit

View File

@@ -6,6 +6,10 @@
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
3EAA00032F0000000000AA03 /* Aptabase in Frameworks */ = {isa = PBXBuildFile; productRef = 3EAA00022F0000000000AA02 /* Aptabase */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
3E37F65B2E93FB6F00836187 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
@@ -30,10 +34,6 @@
};
/* End PBXContainerItemProxy section */
/* Begin PBXBuildFile section */
3EAA00032F0000000000AA03 /* Aptabase in Frameworks */ = {isa = PBXBuildFile; productRef = 3EAA00022F0000000000AA02 /* Aptabase */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
3E37F6552E93FB6F00836187 /* CableUITestsScreenshot.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CableUITestsScreenshot.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3E5C0BCC2E72C0FD00247EC8 /* Cable.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Cable.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -418,7 +418,7 @@
CODE_SIGN_ENTITLEMENTS = Cable/Cable.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 41;
CURRENT_PROJECT_VERSION = 84;
DEVELOPMENT_TEAM = RE4FXQ754N;
ENABLE_APP_SANDBOX = YES;
ENABLE_PREVIEWS = YES;
@@ -436,7 +436,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.5.1;
MARKETING_VERSION = 1.7.0;
PRODUCT_BUNDLE_IDENTIFIER = app.voltplan.CableApp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -454,7 +454,7 @@
CODE_SIGN_ENTITLEMENTS = Cable/Cable.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 41;
CURRENT_PROJECT_VERSION = 84;
DEVELOPMENT_TEAM = RE4FXQ754N;
ENABLE_APP_SANDBOX = YES;
ENABLE_PREVIEWS = YES;
@@ -472,7 +472,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.5.1;
MARKETING_VERSION = 1.7.0;
PRODUCT_BUNDLE_IDENTIFIER = app.voltplan.CableApp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@@ -16,6 +16,11 @@ class AppDelegate: NSObject, UIApplicationDelegate {
appKey: "A-SH-4260269603",
with: InitOptions(host: "https://apta.yuzuhub.com")
)
let isFirstLaunch = !UserDefaults.standard.bool(forKey: "hasLaunchedBefore")
if isFirstLaunch {
UserDefaults.standard.set(true, forKey: "hasLaunchedBefore")
AnalyticsTracker.log("First Launch")
}
AnalyticsTracker.log("App Launched")
return true
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "Cable-iOS-Default-1024x1024@1x.png",
"filename" : "AppIcon-iOS-Default-1024x1024@1x.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "AppIcon-iOS-Default-1024x1024@1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -330,3 +330,55 @@
"cable.pro.trial.duration.month.plural" = "%@-month";
"cable.pro.trial.duration.year.singular" = "%@-year";
"cable.pro.trial.duration.year.plural" = "%@-year";
// MARK: - PDF Overview Export
"overview.pdf.loads.section" = "Loads";
"overview.pdf.batteries.section" = "Batteries";
"overview.pdf.chargers.section" = "Chargers";
"overview.pdf.diagram.chargers" = "CHARGERS";
"overview.pdf.diagram.batteries" = "BATTERY BANK";
"overview.pdf.diagram.loads" = "LOADS";
"overview.pdf.diagram.no.batteries" = "No batteries";
"overview.pdf.diagram.usable" = "Usable";
"overview.pdf.summary.title" = "System Summary";
"overview.pdf.summary.runtime" = "Estimated Runtime";
"overview.pdf.summary.chargetime" = "Charge Time";
"overview.pdf.summary.totalpower" = "Total Power";
"overview.pdf.summary.totalcurrent" = "Total Current";
"overview.pdf.summary.batterycapacity" = "Battery Capacity";
"overview.pdf.summary.chargerpower" = "Charger Power";
"overview.pdf.load.voltage" = "Voltage";
"overview.pdf.load.current" = "Current";
"overview.pdf.load.power" = "Power";
"overview.pdf.load.length" = "Cable Length";
"overview.pdf.load.cable" = "Cable Size";
"overview.pdf.load.vdrop" = "Voltage Drop";
"overview.pdf.load.ploss" = "Power Loss";
"overview.pdf.load.fuse" = "Fuse";
"overview.pdf.battery.chemistry" = "Chemistry";
"overview.pdf.battery.voltage" = "Voltage";
"overview.pdf.battery.capacity" = "Capacity";
"overview.pdf.battery.usable" = "Usable Capacity";
"overview.pdf.battery.energy" = "Energy";
"overview.pdf.battery.usableenergy" = "Usable Energy";
"overview.pdf.charger.input" = "Input Voltage";
"overview.pdf.charger.output" = "Output Voltage";
"overview.pdf.charger.current" = "Max Current";
"overview.pdf.charger.power" = "Power";
"overview.pdf.continued" = "Continued";
"overview.pdf.page.number" = "Page %d";
"overview.share.button.title" = "Share System Overview";
"overview.share.button.subtitle" = "Export as PDF";
"overview.share.error.title" = "Export Failed";
// MARK: - Charger Power Source Type
"charger.editor.source.type" = "Power Source";
"charger.source.shore" = "Shore Power";
"charger.source.solar" = "Solar";
"charger.source.wind" = "Wind";
"charger.source.generator" = "Generator";
// MARK: - Share Menu
"overview.share.diagram" = "Wiring Diagram";
"overview.share.pdf" = "Full Report (PDF)";
"overview.share.diagram.error" = "Could not generate diagram. Check your internet connection.";

View File

@@ -11,6 +11,7 @@ struct ChargerConfiguration: Identifiable, Hashable {
var iconName: String
var colorName: String
var system: ElectricalSystem
var powerSourceType: SavedCharger.PowerSourceType
init(
id: UUID = UUID(),
@@ -21,7 +22,8 @@ struct ChargerConfiguration: Identifiable, Hashable {
maxPowerWatts: Double = 0.0,
iconName: String = "bolt.fill",
colorName: String = "orange",
system: ElectricalSystem
system: ElectricalSystem,
powerSourceType: SavedCharger.PowerSourceType = .shore
) {
self.id = id
self.name = name
@@ -32,6 +34,7 @@ struct ChargerConfiguration: Identifiable, Hashable {
self.iconName = iconName
self.colorName = colorName
self.system = system
self.powerSourceType = powerSourceType
}
init(savedCharger: SavedCharger, system: ElectricalSystem) {
@@ -44,6 +47,7 @@ struct ChargerConfiguration: Identifiable, Hashable {
self.iconName = savedCharger.iconName
self.colorName = savedCharger.colorName
self.system = system
self.powerSourceType = savedCharger.sourceType
}
var effectivePowerWatts: Double {
@@ -62,6 +66,7 @@ struct ChargerConfiguration: Identifiable, Hashable {
savedCharger.iconName = iconName
savedCharger.colorName = colorName
savedCharger.system = system
savedCharger.powerSourceType = powerSourceType.rawValue
savedCharger.timestamp = Date()
}
}

View File

@@ -346,6 +346,7 @@ struct ChargerEditorView: View {
VStack(spacing: 0) {
headerInfoBar
List {
powerSourceSection
electricalSection
chargingSection
}
@@ -597,6 +598,21 @@ struct ChargerEditorView: View {
.accessibilityLabel(appearanceAccessibilityLabel)
}
private var powerSourceSection: some View {
Section {
Picker(
String(localized: "charger.editor.source.type", defaultValue: "Power Source"),
selection: $configuration.powerSourceType
) {
ForEach(SavedCharger.PowerSourceType.allCases) { type in
Label(type.displayName, systemImage: type.iconName)
.tag(type)
}
}
.pickerStyle(.menu)
}
}
private var electricalSection: some View {
Section {
SliderSection(

View File

@@ -18,6 +18,39 @@ final class SavedCharger {
var affiliateCountryCode: String?
var bomCompletedItemIDs: [String] = []
var identifier: String
var powerSourceType: String = "shore"
enum PowerSourceType: String, CaseIterable, Identifiable {
case shore = "shore"
case solar = "solar"
case wind = "wind"
case generator = "generator"
var id: Self { self }
var displayName: String {
switch self {
case .shore: return String(localized: "charger.source.shore", defaultValue: "Shore Power")
case .solar: return String(localized: "charger.source.solar", defaultValue: "Solar")
case .wind: return String(localized: "charger.source.wind", defaultValue: "Wind")
case .generator: return String(localized: "charger.source.generator", defaultValue: "Generator")
}
}
var iconName: String {
switch self {
case .shore: return "powerplug"
case .solar: return "sun.max.fill"
case .wind: return "wind"
case .generator: return "engine.combustion.fill"
}
}
}
var sourceType: PowerSourceType {
get { PowerSourceType(rawValue: powerSourceType) ?? .shore }
set { powerSourceType = newValue.rawValue }
}
init(
id: UUID = UUID(),
@@ -34,7 +67,8 @@ final class SavedCharger {
affiliateURLString: String? = nil,
affiliateCountryCode: String? = nil,
bomCompletedItemIDs: [String] = [],
identifier: String = UUID().uuidString
identifier: String = UUID().uuidString,
powerSourceType: String = "shore"
) {
self.id = id
self.name = name
@@ -51,6 +85,7 @@ final class SavedCharger {
self.affiliateCountryCode = affiliateCountryCode
self.bomCompletedItemIDs = bomCompletedItemIDs
self.identifier = identifier
self.powerSourceType = powerSourceType
}
var effectivePowerWatts: Double {

View File

@@ -26,6 +26,11 @@ struct LoadsView: View {
@State private var chargerDraft: ChargerConfiguration?
@State private var activeStatus: LoadConfigurationStatus?
@State private var editMode: EditMode = .inactive
@State private var overviewExportRequested = false
@State private var diagramExportRequested = false
@State private var isExportingOverview = false
@State private var overviewShareItem: OverviewShareItem?
@State private var overviewExportError: OverviewExportError?
let system: ElectricalSystem
private let presentSystemEditorOnAppear: Bool
@@ -165,7 +170,34 @@ struct LoadsView: View {
let showEditBatteries = selectedComponentTab == .batteries && !savedBatteries.isEmpty
let showEditChargers = selectedComponentTab == .chargers && !savedChargers.isEmpty
if showPrimary || showEditLoads || showEditBatteries || showEditChargers {
if selectedComponentTab == .overview {
if isExportingOverview {
ProgressView()
.progressViewStyle(.circular)
} else {
Menu {
Button {
diagramExportRequested = true
} label: {
Label(
String(localized: "overview.share.diagram", defaultValue: "Wiring Diagram"),
systemImage: "bolt.horizontal"
)
}
Button {
overviewExportRequested = true
} label: {
Label(
String(localized: "overview.share.pdf", defaultValue: "Full Report (PDF)"),
systemImage: "doc.richtext"
)
}
} label: {
Image(systemName: "square.and.arrow.up")
}
.accessibilityIdentifier("system-overview-share-button")
}
} else if showPrimary || showEditLoads || showEditBatteries || showEditChargers {
HStack {
if showPrimary {
Button(action: {
@@ -209,6 +241,36 @@ struct LoadsView: View {
}
)
}
.sheet(item: $overviewShareItem, onDismiss: cleanupOverviewShareItem) { item in
ShareSheet(items: item.shareItems)
}
.alert(
String(localized: "overview.share.error.title", defaultValue: "Export Failed"),
isPresented: Binding<Bool>(
get: { overviewExportError != nil },
set: { if !$0 { overviewExportError = nil } }
)
) {
Button(String(localized: "generic.ok", defaultValue: "OK")) {
overviewExportError = nil
}
} message: {
if let error = overviewExportError {
Text(error.message)
}
}
.onChange(of: overviewExportRequested) { _, requested in
if requested {
overviewExportRequested = false
exportOverviewPDF()
}
}
.onChange(of: diagramExportRequested) { _, requested in
if requested {
diagramExportRequested = false
exportDiagramImage()
}
}
.sheet(isPresented: $showingComponentLibrary) {
ComponentLibraryView { item in
addComponent(item)
@@ -1004,4 +1066,205 @@ struct LoadsView: View {
case batteries
case chargers
}
// MARK: - PDF Export
private struct OverviewShareItem: Identifiable {
let id = UUID()
let shareItems: [Any]
let tempURL: URL?
}
private struct OverviewExportError: Identifiable {
let message: String
var id: String { message }
}
private func exportOverviewPDF() {
guard !isExportingOverview else { return }
isExportingOverview = true
let snapshot = buildSnapshot()
Task {
do {
// Fetch diagram image from VoltPlan API (falls back to Core Graphics if unavailable)
let diagramImage = await SystemOverviewPDFExporter.fetchDiagramImage(snapshot: snapshot)
let exporter = SystemOverviewPDFExporter()
let url = try exporter.export(snapshot: snapshot, diagramImage: diagramImage)
AnalyticsTracker.log("Overview PDF Shared", properties: [
"system": snapshot.systemName,
])
await MainActor.run {
overviewShareItem = OverviewShareItem(shareItems: [url], tempURL: url)
isExportingOverview = false
}
} catch {
await MainActor.run {
overviewExportError = OverviewExportError(message: error.localizedDescription)
isExportingOverview = false
}
}
}
}
private func exportDiagramImage() {
guard !isExportingOverview else { return }
isExportingOverview = true
let snapshot = buildSnapshot()
Task {
let diagramImage = await SystemOverviewPDFExporter.fetchDiagramImage(snapshot: snapshot)
await MainActor.run {
if let image = diagramImage,
let opaqueImage = Self.imageWithWhiteBackground(image),
let pngData = opaqueImage.pngData() {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("\(snapshot.systemName)-Diagram.png")
try? pngData.write(to: url, options: .atomic)
AnalyticsTracker.log("Diagram Image Shared", properties: [
"system": snapshot.systemName,
])
overviewShareItem = OverviewShareItem(shareItems: [url], tempURL: url)
} else {
overviewExportError = OverviewExportError(
message: String(localized: "overview.share.diagram.error", defaultValue: "Could not generate diagram. Check your internet connection.")
)
}
isExportingOverview = false
}
}
}
private static func imageWithWhiteBackground(_ image: UIImage) -> UIImage? {
let size = image.size
UIGraphicsBeginImageContextWithOptions(size, true, image.scale)
defer { UIGraphicsEndImageContext() }
UIColor.white.setFill()
UIRectFill(CGRect(origin: .zero, size: size))
image.draw(at: .zero)
// Draw app icon as subtle watermark in bottom-right corner
if let appIcon = UIImage(named: "AppIconWatermark") {
let watermarkSize: CGFloat = min(size.width, size.height) * 0.08
let margin: CGFloat = watermarkSize * 0.4
let watermarkRect = CGRect(
x: size.width - watermarkSize - margin,
y: size.height - watermarkSize - margin,
width: watermarkSize,
height: watermarkSize
)
appIcon.draw(in: watermarkRect, blendMode: .normal, alpha: 0.15)
}
return UIGraphicsGetImageFromCurrentImageContext()
}
private func buildSnapshot() -> SystemOverviewPDFExporter.SystemSnapshot {
let currentUnitSystem = unitSettings.unitSystem
let loads = savedLoads
let batteries = savedBatteries
let chargers = savedChargers
let loadSnapshots = loads.map { load in
SystemOverviewPDFExporter.LoadSnapshot(
name: load.name,
voltage: load.voltage,
current: load.current,
power: load.power,
length: load.length,
crossSection: load.crossSection,
dutyCyclePercent: load.dutyCyclePercent,
dailyUsageHours: load.dailyUsageHours,
recommendedCrossSection: ElectricalCalculations.recommendedCrossSection(
length: load.length, current: load.current, voltage: load.voltage, unitSystem: currentUnitSystem
),
voltageDrop: ElectricalCalculations.voltageDrop(
length: load.length, current: load.current, voltage: load.voltage, unitSystem: currentUnitSystem
),
voltageDropPercent: ElectricalCalculations.voltageDropPercentage(
length: load.length, current: load.current, voltage: load.voltage, unitSystem: currentUnitSystem
),
powerLoss: ElectricalCalculations.powerLoss(
length: load.length, current: load.current, voltage: load.voltage, unitSystem: currentUnitSystem
),
recommendedFuse: ElectricalCalculations.recommendedFuse(forCurrent: load.current),
iconUrl: load.remoteIconURLString
)
}
let batterySnapshots = batteries.map { battery in
SystemOverviewPDFExporter.BatterySnapshot(
name: battery.name,
chemistry: battery.chemistry.rawValue,
nominalVoltage: battery.nominalVoltage,
capacityAmpHours: battery.capacityAmpHours,
usableCapacityAmpHours: battery.usableCapacityAmpHours,
energyWattHours: battery.energyWattHours,
usableEnergyWattHours: battery.usableEnergyWattHours,
iconUrl: nil
)
}
let chargerSnapshots = chargers.map { charger in
SystemOverviewPDFExporter.ChargerSnapshot(
name: charger.name,
inputVoltage: charger.inputVoltage,
outputVoltage: charger.outputVoltage,
maxCurrentAmps: charger.maxCurrentAmps,
effectivePowerWatts: charger.effectivePowerWatts,
sourceType: charger.sourceType.rawValue,
iconUrl: charger.remoteIconURLString
)
}
let totalPower = loads.reduce(0.0) { $0 + $1.power }
let totalCurrent = loads.reduce(0.0) { $0 + $1.current }
let totalCapacity = batteries.reduce(0.0) { $0 + $1.capacityAmpHours }
let totalEnergy = batteries.reduce(0.0) { $0 + $1.energyWattHours }
let totalUsableCapacity = batteries.reduce(0.0) { $0 + $1.usableCapacityAmpHours }
let totalUsableEnergy = batteries.reduce(0.0) { $0 + $1.usableEnergyWattHours }
let totalChargerPower = chargers.reduce(0.0) { $0 + $1.effectivePowerWatts }
let totalChargerCurrent = chargers.reduce(0.0) { $0 + $1.maxCurrentAmps }
var runtimeFormatted: String? = nil
if totalPower > 0 && totalUsableEnergy > 0 {
let hours = totalUsableEnergy / totalPower
runtimeFormatted = String(format: "%.1f h", hours)
}
var chargeTimeFormatted: String? = nil
if totalChargerPower > 0 && totalEnergy > 0 {
let hours = totalEnergy / totalChargerPower
chargeTimeFormatted = String(format: "%.1f h", hours)
}
return SystemOverviewPDFExporter.SystemSnapshot(
systemName: system.name,
unitSystem: currentUnitSystem,
loads: loadSnapshots,
batteries: batterySnapshots,
chargers: chargerSnapshots,
estimatedRuntimeFormatted: runtimeFormatted,
estimatedChargeTimeFormatted: chargeTimeFormatted,
totalPower: totalPower,
totalCurrent: totalCurrent,
totalBatteryCapacity: totalCapacity,
totalBatteryEnergy: totalEnergy,
totalUsableCapacity: totalUsableCapacity,
totalUsableEnergy: totalUsableEnergy,
totalChargerPower: totalChargerPower,
totalChargerCurrent: totalChargerCurrent
)
}
private func cleanupOverviewShareItem() {
guard let item = overviewShareItem else { return }
overviewShareItem = nil
if let url = item.tempURL {
try? FileManager.default.removeItem(at: url)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -163,6 +163,7 @@ struct SystemOverviewView: View {
accessibilityIdentifier: "system-bom-button"
)
}
.padding(.top, 4)
}
@@ -1420,6 +1421,7 @@ struct SystemOverviewView: View {
}
}
}
}
private struct GoalEditorSheet: View {

View File

@@ -395,3 +395,55 @@
"• 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";
// MARK: - PDF Overview Export
"overview.pdf.loads.section" = "Verbraucher";
"overview.pdf.batteries.section" = "Batterien";
"overview.pdf.chargers.section" = "Ladegeräte";
"overview.pdf.diagram.chargers" = "LADEGERÄTE";
"overview.pdf.diagram.batteries" = "BATTERIEBANK";
"overview.pdf.diagram.loads" = "VERBRAUCHER";
"overview.pdf.diagram.no.batteries" = "Keine Batterien";
"overview.pdf.diagram.usable" = "Nutzbar";
"overview.pdf.summary.title" = "Systemübersicht";
"overview.pdf.summary.runtime" = "Geschätzte Laufzeit";
"overview.pdf.summary.chargetime" = "Ladezeit";
"overview.pdf.summary.totalpower" = "Gesamtleistung";
"overview.pdf.summary.totalcurrent" = "Gesamtstrom";
"overview.pdf.summary.batterycapacity" = "Batteriekapazität";
"overview.pdf.summary.chargerpower" = "Ladeleistung";
"overview.pdf.load.voltage" = "Spannung";
"overview.pdf.load.current" = "Strom";
"overview.pdf.load.power" = "Leistung";
"overview.pdf.load.length" = "Kabellänge";
"overview.pdf.load.cable" = "Kabelquerschnitt";
"overview.pdf.load.vdrop" = "Spannungsabfall";
"overview.pdf.load.ploss" = "Leistungsverlust";
"overview.pdf.load.fuse" = "Sicherung";
"overview.pdf.battery.chemistry" = "Chemie";
"overview.pdf.battery.voltage" = "Spannung";
"overview.pdf.battery.capacity" = "Kapazität";
"overview.pdf.battery.usable" = "Nutzbare Kapazität";
"overview.pdf.battery.energy" = "Energie";
"overview.pdf.battery.usableenergy" = "Nutzbare Energie";
"overview.pdf.charger.input" = "Eingangsspannung";
"overview.pdf.charger.output" = "Ausgangsspannung";
"overview.pdf.charger.current" = "Max. Strom";
"overview.pdf.charger.power" = "Leistung";
"overview.pdf.continued" = "Fortsetzung";
"overview.pdf.page.number" = "Seite %d";
"overview.share.button.title" = "Systemübersicht teilen";
"overview.share.button.subtitle" = "Als PDF exportieren";
"overview.share.error.title" = "Export fehlgeschlagen";
// MARK: - Charger Power Source Type
"charger.editor.source.type" = "Stromquelle";
"charger.source.shore" = "Landstrom";
"charger.source.solar" = "Solar";
"charger.source.wind" = "Wind";
"charger.source.generator" = "Generator";
// MARK: - Share Menu
"overview.share.diagram" = "Schaltplan";
"overview.share.pdf" = "Vollständiger Bericht (PDF)";
"overview.share.diagram.error" = "Schaltplan konnte nicht erstellt werden. Überprüfe deine Internetverbindung.";

View File

@@ -357,3 +357,55 @@
"cable.pro.feature.batteryCapacity" = "Configura la capacidad utilizable de la batería";
"cable.pro.feature.usageBased" = "Cálculos basados en el uso";
"generic.ok" = "Aceptar";
// MARK: - PDF Overview Export
"overview.pdf.loads.section" = "Cargas";
"overview.pdf.batteries.section" = "Baterías";
"overview.pdf.chargers.section" = "Cargadores";
"overview.pdf.diagram.chargers" = "CARGADORES";
"overview.pdf.diagram.batteries" = "BANCO DE BATERÍAS";
"overview.pdf.diagram.loads" = "CARGAS";
"overview.pdf.diagram.no.batteries" = "Sin baterías";
"overview.pdf.diagram.usable" = "Utilizable";
"overview.pdf.summary.title" = "Resumen del sistema";
"overview.pdf.summary.runtime" = "Autonomía estimada";
"overview.pdf.summary.chargetime" = "Tiempo de carga";
"overview.pdf.summary.totalpower" = "Potencia total";
"overview.pdf.summary.totalcurrent" = "Corriente total";
"overview.pdf.summary.batterycapacity" = "Capacidad de batería";
"overview.pdf.summary.chargerpower" = "Potencia de carga";
"overview.pdf.load.voltage" = "Tensión";
"overview.pdf.load.current" = "Corriente";
"overview.pdf.load.power" = "Potencia";
"overview.pdf.load.length" = "Longitud del cable";
"overview.pdf.load.cable" = "Sección del cable";
"overview.pdf.load.vdrop" = "Caída de tensión";
"overview.pdf.load.ploss" = "Pérdida de potencia";
"overview.pdf.load.fuse" = "Fusible";
"overview.pdf.battery.chemistry" = "Química";
"overview.pdf.battery.voltage" = "Tensión";
"overview.pdf.battery.capacity" = "Capacidad";
"overview.pdf.battery.usable" = "Capacidad utilizable";
"overview.pdf.battery.energy" = "Energía";
"overview.pdf.battery.usableenergy" = "Energía utilizable";
"overview.pdf.charger.input" = "Tensión de entrada";
"overview.pdf.charger.output" = "Tensión de salida";
"overview.pdf.charger.current" = "Corriente máx.";
"overview.pdf.charger.power" = "Potencia";
"overview.pdf.continued" = "Continuación";
"overview.pdf.page.number" = "Página %d";
"overview.share.button.title" = "Compartir resumen del sistema";
"overview.share.button.subtitle" = "Exportar como PDF";
"overview.share.error.title" = "Error en la exportación";
// MARK: - Charger Power Source Type
"charger.editor.source.type" = "Fuente de energía";
"charger.source.shore" = "Corriente de tierra";
"charger.source.solar" = "Solar";
"charger.source.wind" = "Eólica";
"charger.source.generator" = "Generador";
// MARK: - Share Menu
"overview.share.diagram" = "Diagrama de cableado";
"overview.share.pdf" = "Informe completo (PDF)";
"overview.share.diagram.error" = "No se pudo generar el diagrama. Comprueba tu conexión a Internet.";

View File

@@ -357,3 +357,55 @@
"cable.pro.feature.batteryCapacity" = "Configurez la capacité utilisable de la batterie";
"cable.pro.feature.usageBased" = "Calculs basés sur lutilisation";
"generic.ok" = "OK";
// MARK: - PDF Overview Export
"overview.pdf.loads.section" = "Charges";
"overview.pdf.batteries.section" = "Batteries";
"overview.pdf.chargers.section" = "Chargeurs";
"overview.pdf.diagram.chargers" = "CHARGEURS";
"overview.pdf.diagram.batteries" = "BANC DE BATTERIES";
"overview.pdf.diagram.loads" = "CHARGES";
"overview.pdf.diagram.no.batteries" = "Aucune batterie";
"overview.pdf.diagram.usable" = "Utilisable";
"overview.pdf.summary.title" = "Résumé du système";
"overview.pdf.summary.runtime" = "Autonomie estimée";
"overview.pdf.summary.chargetime" = "Temps de charge";
"overview.pdf.summary.totalpower" = "Puissance totale";
"overview.pdf.summary.totalcurrent" = "Courant total";
"overview.pdf.summary.batterycapacity" = "Capacité de batterie";
"overview.pdf.summary.chargerpower" = "Puissance de charge";
"overview.pdf.load.voltage" = "Tension";
"overview.pdf.load.current" = "Courant";
"overview.pdf.load.power" = "Puissance";
"overview.pdf.load.length" = "Longueur du câble";
"overview.pdf.load.cable" = "Section du câble";
"overview.pdf.load.vdrop" = "Chute de tension";
"overview.pdf.load.ploss" = "Perte de puissance";
"overview.pdf.load.fuse" = "Fusible";
"overview.pdf.battery.chemistry" = "Chimie";
"overview.pdf.battery.voltage" = "Tension";
"overview.pdf.battery.capacity" = "Capacité";
"overview.pdf.battery.usable" = "Capacité utilisable";
"overview.pdf.battery.energy" = "Énergie";
"overview.pdf.battery.usableenergy" = "Énergie utilisable";
"overview.pdf.charger.input" = "Tension d'entrée";
"overview.pdf.charger.output" = "Tension de sortie";
"overview.pdf.charger.current" = "Courant max.";
"overview.pdf.charger.power" = "Puissance";
"overview.pdf.continued" = "Suite";
"overview.pdf.page.number" = "Page %d";
"overview.share.button.title" = "Partager le résumé du système";
"overview.share.button.subtitle" = "Exporter en PDF";
"overview.share.error.title" = "Échec de l'exportation";
// MARK: - Charger Power Source Type
"charger.editor.source.type" = "Source d'énergie";
"charger.source.shore" = "Courant de quai";
"charger.source.solar" = "Solaire";
"charger.source.wind" = "Éolienne";
"charger.source.generator" = "Groupe électrogène";
// MARK: - Share Menu
"overview.share.diagram" = "Schéma de câblage";
"overview.share.pdf" = "Rapport complet (PDF)";
"overview.share.diagram.error" = "Impossible de générer le schéma. Vérifiez votre connexion Internet.";

View File

@@ -357,3 +357,55 @@
"cable.pro.feature.batteryCapacity" = "Configureer bruikbare batterijcapaciteit";
"cable.pro.feature.usageBased" = "Gebruiksgestuurde berekeningen";
"generic.ok" = "OK";
// MARK: - PDF Overview Export
"overview.pdf.loads.section" = "Verbruikers";
"overview.pdf.batteries.section" = "Accu's";
"overview.pdf.chargers.section" = "Laders";
"overview.pdf.diagram.chargers" = "LADERS";
"overview.pdf.diagram.batteries" = "ACCUBANK";
"overview.pdf.diagram.loads" = "VERBRUIKERS";
"overview.pdf.diagram.no.batteries" = "Geen accu's";
"overview.pdf.diagram.usable" = "Bruikbaar";
"overview.pdf.summary.title" = "Systeemoverzicht";
"overview.pdf.summary.runtime" = "Geschatte looptijd";
"overview.pdf.summary.chargetime" = "Laadtijd";
"overview.pdf.summary.totalpower" = "Totaal vermogen";
"overview.pdf.summary.totalcurrent" = "Totale stroom";
"overview.pdf.summary.batterycapacity" = "Accucapaciteit";
"overview.pdf.summary.chargerpower" = "Laadvermogen";
"overview.pdf.load.voltage" = "Spanning";
"overview.pdf.load.current" = "Stroom";
"overview.pdf.load.power" = "Vermogen";
"overview.pdf.load.length" = "Kabellengte";
"overview.pdf.load.cable" = "Kabeldoorsnede";
"overview.pdf.load.vdrop" = "Spanningsval";
"overview.pdf.load.ploss" = "Vermogensverlies";
"overview.pdf.load.fuse" = "Zekering";
"overview.pdf.battery.chemistry" = "Chemie";
"overview.pdf.battery.voltage" = "Spanning";
"overview.pdf.battery.capacity" = "Capaciteit";
"overview.pdf.battery.usable" = "Bruikbare capaciteit";
"overview.pdf.battery.energy" = "Energie";
"overview.pdf.battery.usableenergy" = "Bruikbare energie";
"overview.pdf.charger.input" = "Ingangsspanning";
"overview.pdf.charger.output" = "Uitgangsspanning";
"overview.pdf.charger.current" = "Max. stroom";
"overview.pdf.charger.power" = "Vermogen";
"overview.pdf.continued" = "Vervolg";
"overview.pdf.page.number" = "Pagina %d";
"overview.share.button.title" = "Systeemoverzicht delen";
"overview.share.button.subtitle" = "Exporteren als PDF";
"overview.share.error.title" = "Export mislukt";
// MARK: - Charger Power Source Type
"charger.editor.source.type" = "Stroombron";
"charger.source.shore" = "Walstroom";
"charger.source.solar" = "Zonne-energie";
"charger.source.wind" = "Wind";
"charger.source.generator" = "Generator";
// MARK: - Share Menu
"overview.share.diagram" = "Bedradingsschema";
"overview.share.pdf" = "Volledig rapport (PDF)";
"overview.share.diagram.error" = "Diagram kon niet worden gegenereerd. Controleer je internetverbinding.";