Fix #23: Tour-Übersetzung – deutsche Strings als Keys, Lokalisierungen ergänzt

- TourCatalog.swift: Technische Keys (tour.onboarding.*) durch deutschen
  Klartext ersetzt (konform mit Projekt-xcstrings-Konvention)
- TourCardView.swift: Ternary-Ausdrucks-Bug behoben (String statt
  LocalizedStringKey); Button-Labels mit deutschen Strings
- SettingsView.swift: settings.tours.* durch deutsche Keys ersetzt
- Localizable.xcstrings: Technische Keys entfernt, alle Tour-Strings als
  deutsche Keys mit EN-Übersetzungen hinzugefügt (19 neue Einträge)
- TourCatalogTests: import Foundation ergänzt (LocalizedStringResource)
- TourCoordinatorTests: import CoreGraphics ergänzt (CGRect)
- StoreTests: Closure-Argument-Fehler behoben (_ in)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 19:33:04 +02:00
parent b214bb6c50
commit a0741ba608
7 changed files with 253 additions and 107 deletions
+226 -83
View File
@@ -228,6 +228,40 @@
} }
} }
}, },
"%lld Schritte" : {
"comment" : "SettingsView tour step count label (e.g. '6 Schritte')",
"localizations" : {
"en" : {
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld step"
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld steps"
}
}
}
}
}
}
},
"%lld von %lld" : {
"comment" : "TourCardView step counter (e.g. '2 von 6')",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld of %lld"
}
}
}
},
"%lld von %lld — Maximum erreicht" : { "%lld von %lld — Maximum erreicht" : {
"localizations" : { "localizations" : {
"de" : { "de" : {
@@ -859,6 +893,17 @@
} }
} }
}, },
"App-Einführung" : {
"comment" : "TourCatalog onboarding tour title shown in SettingsView",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "App Introduction"
}
}
}
},
"App-Schutz" : { "App-Schutz" : {
"comment" : "SettingsView section header for app lock settings", "comment" : "SettingsView section header for app lock settings",
"localizations" : { "localizations" : {
@@ -871,7 +916,15 @@
} }
}, },
"App-Touren" : { "App-Touren" : {
"comment" : "SettingsView Tour section header",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "App Tours"
}
}
}
}, },
"Arbeit" : { "Arbeit" : {
"comment" : "PersonTag.work raw value", "comment" : "PersonTag.work raw value",
@@ -1467,6 +1520,17 @@
} }
} }
}, },
"Deine Menschen im Mittelpunkt" : {
"comment" : "TourCatalog onboarding step 2 title",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Your People at the Center"
}
}
}
},
"Details" : { "Details" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -1945,6 +2009,17 @@
} }
} }
}, },
"Einblicke, wenn du willst" : {
"comment" : "TourCatalog onboarding step 6 title",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Insights When You Want Them"
}
}
}
},
"Einrichten" : { "Einrichten" : {
"comment" : "CallWindowSetupView setup button label when onboarding", "comment" : "CallWindowSetupView setup button label when onboarding",
"localizations" : { "localizations" : {
@@ -2034,6 +2109,17 @@
} }
} }
}, },
"Erfasse Treffen, Nachrichten und Erlebnisse. So weißt du, worüber ihr das letzte Mal geredet habt." : {
"comment" : "TourCatalog onboarding step 3 body",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Record meetings, messages, and experiences. So you know what you last talked about."
}
}
}
},
"Ergebnis bestätigen und fortfahren" : { "Ergebnis bestätigen und fortfahren" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -2350,8 +2436,23 @@
} }
} }
}, },
"Füge Personen hinzu, die dir wichtig sind. Notiere Interessen, Gesprächsthemen und was euch verbindet." : {
"comment" : "TourCatalog onboarding step 2 body",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Add people who matter to you. Note interests, conversation topics, and what connects you."
}
}
}
},
"Füge zuerst Personen im Tab „Menschen“ hinzu." : { "Füge zuerst Personen im Tab „Menschen“ hinzu." : {
},
"Füge zuerst Personen im Tab „Menschen” hinzu." : {
"comment" : "TodayPersonPickerSheet empty state hint when no contacts exist yet", "comment" : "TodayPersonPickerSheet empty state hint when no contacts exist yet",
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -3270,6 +3371,17 @@
} }
} }
}, },
"Leg Vorhaben an und erhalte eine Erinnerung damit aus 'Wir müssen mal wieder…' ein echtes Treffen wird." : {
"comment" : "TourCatalog onboarding step 4 body",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Create intentions and get a reminder so 'We should catch up…' becomes a real meeting."
}
}
}
},
"Limit erreicht" : { "Limit erreicht" : {
"comment" : "LogbuchView AI refresh button label when at request limit", "comment" : "LogbuchView AI refresh button label when at request limit",
"localizations" : { "localizations" : {
@@ -3335,6 +3447,17 @@
} }
} }
}, },
"Loslegen" : {
"comment" : "TourCardView finish button label on last tour step",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Let's go"
}
}
}
},
"Mag ich" : { "Mag ich" : {
"comment" : "IchView likes preferences field label", "comment" : "IchView likes preferences field label",
"extractionState" : "stale", "extractionState" : "stale",
@@ -3589,6 +3712,17 @@
} }
} }
}, },
"Momente festhalten" : {
"comment" : "TourCatalog onboarding step 3 title",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Capture Moments"
}
}
}
},
"Momente planen und hinzufügen" : { "Momente planen und hinzufügen" : {
"comment" : "TodayView empty state CTA button subtitle", "comment" : "TodayView empty state CTA button subtitle",
"localizations" : { "localizations" : {
@@ -3869,6 +4003,28 @@
} }
} }
}, },
"nahbar erinnert dich, wenn du lange nichts von jemandem gehört hast. Du entscheidest, wie oft." : {
"comment" : "TourCatalog onboarding step 5 body",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "nahbar reminds you when you haven't heard from someone in a while. You decide how often."
}
}
}
},
"nahbar hilft dir, echte Verbindungen zu pflegen ohne Stress, ohne Algorithmen." : {
"comment" : "TourCatalog onboarding step 1 body",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "nahbar helps you nurture real connections without stress, without algorithms."
}
}
}
},
"nahbar Max freischalten für KI-Analyse" : { "nahbar Max freischalten für KI-Analyse" : {
"comment" : "LogbuchView upsell button for AI analysis", "comment" : "LogbuchView upsell button for AI analysis",
"localizations" : { "localizations" : {
@@ -4288,6 +4444,17 @@
} }
} }
}, },
"Optionale KI-Analyse zeigt Muster in deinen Verbindungen. Alles optional deine Daten bleiben bei dir." : {
"comment" : "TourCatalog onboarding step 6 body",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Optional AI analysis shows patterns in your connections. Everything optional your data stays with you."
}
}
}
},
"Passend für dich" : { "Passend für dich" : {
"extractionState" : "stale", "extractionState" : "stale",
"localizations" : { "localizations" : {
@@ -4384,6 +4551,17 @@
} }
} }
}, },
"Plane das Nächste" : {
"comment" : "TourCatalog onboarding step 4 title",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Plan What's Next"
}
}
}
},
"Plane Unternehmungen mit Erinnerung nahbar erinnert dich zur richtigen Zeit." : { "Plane Unternehmungen mit Erinnerung nahbar erinnert dich zur richtigen Zeit." : {
"comment" : "OnboardingContainerView feature tour card description for Unternehmung (intention type)", "comment" : "OnboardingContainerView feature tour card description for Unternehmung (intention type)",
"localizations" : { "localizations" : {
@@ -4546,6 +4724,17 @@
} }
} }
}, },
"Sanfte Erinnerungen" : {
"comment" : "TourCatalog onboarding step 5 title",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Gentle Reminders"
}
}
}
},
"Schließen" : { "Schließen" : {
"comment" : "PersonDetailView / ShareExtensionView close button", "comment" : "PersonDetailView / ShareExtensionView close button",
"localizations" : { "localizations" : {
@@ -4691,32 +4880,6 @@
} }
} }
}, },
"settings.tours.start" : {
},
"settings.tours.stepCount %lld" : {
"comment" : "SettingsView step count label (e.g. '6 Schritte')",
"localizations" : {
"en" : {
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld step"
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld steps"
}
}
}
}
}
}
},
"Signal" : { "Signal" : {
"comment" : "MomentSource.signal raw value", "comment" : "MomentSource.signal raw value",
"extractionState" : "stale", "extractionState" : "stale",
@@ -5036,69 +5199,38 @@
} }
} }
}, },
"tour.common.back" : { "Tour schließen" : {
"comment" : "TourCardView accessibility label for close button",
},
"tour.common.close" : {
},
"tour.common.finish" : {
},
"tour.common.next" : {
},
"tour.common.skip" : {
},
"tour.common.stepCounter %lld %lld" : {
"localizations" : { "localizations" : {
"de" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "translated",
"value" : "tour.common.stepCounter %1$lld %2$lld" "value" : "Close tour"
} }
} }
} }
}, },
"tour.onboarding.step1.body" : { "Tour starten" : {
"comment" : "SettingsView button to replay a tour",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Start tour"
}
}
}
}, },
"tour.onboarding.step1.title" : { "Tour überspringen" : {
"comment" : "TourCardView skip button label",
}, "localizations" : {
"tour.onboarding.step2.body" : { "en" : {
"stringUnit" : {
}, "state" : "translated",
"tour.onboarding.step2.title" : { "value" : "Skip tour"
}
}, }
"tour.onboarding.step3.body" : { }
},
"tour.onboarding.step3.title" : {
},
"tour.onboarding.step4.body" : {
},
"tour.onboarding.step4.title" : {
},
"tour.onboarding.step5.body" : {
},
"tour.onboarding.step5.title" : {
},
"tour.onboarding.step6.body" : {
},
"tour.onboarding.step6.title" : {
},
"tour.onboarding.title" : {
}, },
"Treffen" : { "Treffen" : {
"comment" : "MomentType.meeting rawValue + VisitHistorySection / SettingsView section header", "comment" : "MomentType.meeting rawValue + VisitHistorySection / SettingsView section header",
@@ -6135,6 +6267,17 @@
} }
} }
}, },
"Zurück" : {
"comment" : "TourCardView back button label",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Back"
}
}
}
},
"Zusammen essen" : { "Zusammen essen" : {
"comment" : "PersonDetailView activity suggestion: have a meal together (group)", "comment" : "PersonDetailView activity suggestion: have a meal together (group)",
"extractionState" : "stale", "extractionState" : "stale",
+2 -2
View File
@@ -608,12 +608,12 @@ struct SettingsView: View {
Text(tour.title) Text(tour.title)
.font(.system(size: 15)) .font(.system(size: 15))
.foregroundStyle(theme.contentPrimary) .foregroundStyle(theme.contentPrimary)
Text(String.localizedStringWithFormat(String(localized: "settings.tours.stepCount %lld"), Int64(tour.steps.count))) Text(String.localizedStringWithFormat(String(localized: "%lld Schritte"), Int64(tour.steps.count)))
.font(.system(size: 12)) .font(.system(size: 12))
.foregroundStyle(theme.contentTertiary) .foregroundStyle(theme.contentTertiary)
} }
Spacer() Spacer()
Button(String(localized: "settings.tours.start")) { Button("Tour starten") {
tourCoordinator.start(tour.id) tourCoordinator.start(tour.id)
} }
.font(.system(size: 13, weight: .medium)) .font(.system(size: 13, weight: .medium))
+7 -7
View File
@@ -25,7 +25,7 @@ struct TourCardView: View {
.padding(8) .padding(8)
.background(Color.secondary.opacity(0.12), in: Circle()) .background(Color.secondary.opacity(0.12), in: Circle())
} }
.accessibilityLabel(Text("tour.common.close")) .accessibilityLabel("Tour schließen")
} }
.padding(.horizontal, 20) .padding(.horizontal, 20)
.padding(.top, 18) .padding(.top, 18)
@@ -56,14 +56,13 @@ struct TourCardView: View {
Button { Button {
coordinator.skip() coordinator.skip()
} label: { } label: {
Text("tour.common.skip") Text("Tour überspringen")
.font(.caption) .font(.caption)
.foregroundStyle(.tertiary) .foregroundStyle(.tertiary)
} }
.accessibilityLabel(Text("tour.common.skip"))
Text(verbatim: String.localizedStringWithFormat( Text(verbatim: String.localizedStringWithFormat(
String(localized: "tour.common.stepCounter %lld %lld"), String(localized: "%lld von %lld"),
Int64(currentIndex + 1), Int64(currentIndex + 1),
Int64(totalSteps) Int64(totalSteps)
)) ))
@@ -78,18 +77,19 @@ struct TourCardView: View {
Button { Button {
coordinator.previous() coordinator.previous()
} label: { } label: {
Text("tour.common.back") Text("Zurück")
.font(.subheadline.weight(.medium)) .font(.subheadline.weight(.medium))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
.padding(.trailing, 8) .padding(.trailing, 8)
} }
// Next / Finish button // Next / Finish button explicit LocalizedStringKey to ensure lookup
let nextLabel: LocalizedStringKey = coordinator.isLastStep ? "Loslegen" : "Weiter"
Button { Button {
coordinator.next() coordinator.next()
} label: { } label: {
Text(coordinator.isLastStep ? "tour.common.finish" : "tour.common.next") Text(nextLabel)
.font(.subheadline.weight(.semibold)) .font(.subheadline.weight(.semibold))
.foregroundStyle(.white) .foregroundStyle(.white)
.padding(.horizontal, 18) .padding(.horizontal, 18)
+14 -13
View File
@@ -3,6 +3,7 @@ import Foundation
// MARK: - TourCatalog // MARK: - TourCatalog
/// Static registry of all tours defined in the app. /// Static registry of all tours defined in the app.
/// Strings use German text as keys (consistent with project xcstrings convention).
/// New tours are added as static properties and included in `all`. /// New tours are added as static properties and included in `all`.
enum TourCatalog { enum TourCatalog {
@@ -10,41 +11,41 @@ enum TourCatalog {
static let onboarding = Tour( static let onboarding = Tour(
id: .onboarding, id: .onboarding,
title: "tour.onboarding.title", title: "App-Einführung",
steps: [ steps: [
TourStep( TourStep(
title: "tour.onboarding.step1.title", title: "Willkommen bei nahbar",
body: "tour.onboarding.step1.body", body: "nahbar hilft dir, echte Verbindungen zu pflegen ohne Stress, ohne Algorithmen.",
target: nil, target: nil,
preferredCardPosition: .center preferredCardPosition: .center
), ),
TourStep( TourStep(
title: "tour.onboarding.step2.title", title: "Deine Menschen im Mittelpunkt",
body: "tour.onboarding.step2.body", body: "Füge Personen hinzu, die dir wichtig sind. Notiere Interessen, Gesprächsthemen und was euch verbindet.",
target: nil, target: nil,
preferredCardPosition: .center preferredCardPosition: .center
), ),
TourStep( TourStep(
title: "tour.onboarding.step3.title", title: "Momente festhalten",
body: "tour.onboarding.step3.body", body: "Erfasse Treffen, Nachrichten und Erlebnisse. So weißt du, worüber ihr das letzte Mal geredet habt.",
target: nil, target: nil,
preferredCardPosition: .center preferredCardPosition: .center
), ),
TourStep( TourStep(
title: "tour.onboarding.step4.title", title: "Plane das Nächste",
body: "tour.onboarding.step4.body", body: "Leg Vorhaben an und erhalte eine Erinnerung damit aus 'Wir müssen mal wieder…' ein echtes Treffen wird.",
target: nil, target: nil,
preferredCardPosition: .center preferredCardPosition: .center
), ),
TourStep( TourStep(
title: "tour.onboarding.step5.title", title: "Sanfte Erinnerungen",
body: "tour.onboarding.step5.body", body: "nahbar erinnert dich, wenn du lange nichts von jemandem gehört hast. Du entscheidest, wie oft.",
target: nil, target: nil,
preferredCardPosition: .center preferredCardPosition: .center
), ),
TourStep( TourStep(
title: "tour.onboarding.step6.title", title: "Einblicke, wenn du willst",
body: "tour.onboarding.step6.body", body: "Optionale KI-Analyse zeigt Muster in deinen Verbindungen. Alles optional deine Daten bleiben bei dir.",
target: nil, target: nil,
preferredCardPosition: .center preferredCardPosition: .center
), ),
+1 -1
View File
@@ -483,7 +483,7 @@ struct InterestTagHelperSuggestionsTests {
@Test("Duplikate werden dedupliziert") @Test("Duplikate werden dedupliziert")
func deduplicates() { func deduplicates() {
let result = InterestTagHelper.allSuggestions(from: [], likes: "Kino, Musik", dislikes: "Kino") let result = InterestTagHelper.allSuggestions(from: [], likes: "Kino, Musik", dislikes: "Kino")
#expect(!result.contains { result.filter { $0 == "Kino" }.count > 1 }) #expect(!result.contains { _ in result.filter { $0 == "Kino" }.count > 1 })
#expect(result.filter { $0 == "Kino" }.count == 1) #expect(result.filter { $0 == "Kino" }.count == 1)
} }
} }
@@ -1,3 +1,4 @@
import Foundation
import Testing import Testing
@testable import nahbar @testable import nahbar
@@ -1,5 +1,6 @@
import Testing import CoreGraphics
import Foundation import Foundation
import Testing
@testable import nahbar @testable import nahbar
// MARK: - TourCoordinator Tests // MARK: - TourCoordinator Tests