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" : {
"localizations" : {
"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" : {
"comment" : "SettingsView section header for app lock settings",
"localizations" : {
@@ -871,7 +916,15 @@
}
},
"App-Touren" : {
"comment" : "SettingsView Tour section header",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "App Tours"
}
}
}
},
"Arbeit" : {
"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" : {
"localizations" : {
"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" : {
"comment" : "CallWindowSetupView setup button label when onboarding",
"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" : {
"localizations" : {
"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." : {
"comment" : "TodayPersonPickerSheet empty state hint when no contacts exist yet",
"extractionState" : "stale",
"localizations" : {
"en" : {
"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" : {
"comment" : "LogbuchView AI refresh button label when at request limit",
"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" : {
"comment" : "IchView likes preferences field label",
"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" : {
"comment" : "TodayView empty state CTA button subtitle",
"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" : {
"comment" : "LogbuchView upsell button for AI analysis",
"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" : {
"extractionState" : "stale",
"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." : {
"comment" : "OnboardingContainerView feature tour card description for Unternehmung (intention type)",
"localizations" : {
@@ -4546,6 +4724,17 @@
}
}
},
"Sanfte Erinnerungen" : {
"comment" : "TourCatalog onboarding step 5 title",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Gentle Reminders"
}
}
}
},
"Schließen" : {
"comment" : "PersonDetailView / ShareExtensionView close button",
"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" : {
"comment" : "MomentSource.signal raw value",
"extractionState" : "stale",
@@ -5036,69 +5199,38 @@
}
}
},
"tour.common.back" : {
},
"tour.common.close" : {
},
"tour.common.finish" : {
},
"tour.common.next" : {
},
"tour.common.skip" : {
},
"tour.common.stepCounter %lld %lld" : {
"Tour schließen" : {
"comment" : "TourCardView accessibility label for close button",
"localizations" : {
"de" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "tour.common.stepCounter %1$lld %2$lld"
"state" : "translated",
"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.onboarding.step2.body" : {
},
"tour.onboarding.step2.title" : {
},
"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" : {
"Tour überspringen" : {
"comment" : "TourCardView skip button label",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Skip tour"
}
}
}
},
"Treffen" : {
"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" : {
"comment" : "PersonDetailView activity suggestion: have a meal together (group)",
"extractionState" : "stale",
+2 -2
View File
@@ -608,12 +608,12 @@ struct SettingsView: View {
Text(tour.title)
.font(.system(size: 15))
.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))
.foregroundStyle(theme.contentTertiary)
}
Spacer()
Button(String(localized: "settings.tours.start")) {
Button("Tour starten") {
tourCoordinator.start(tour.id)
}
.font(.system(size: 13, weight: .medium))
+7 -7
View File
@@ -25,7 +25,7 @@ struct TourCardView: View {
.padding(8)
.background(Color.secondary.opacity(0.12), in: Circle())
}
.accessibilityLabel(Text("tour.common.close"))
.accessibilityLabel("Tour schließen")
}
.padding(.horizontal, 20)
.padding(.top, 18)
@@ -56,14 +56,13 @@ struct TourCardView: View {
Button {
coordinator.skip()
} label: {
Text("tour.common.skip")
Text("Tour überspringen")
.font(.caption)
.foregroundStyle(.tertiary)
}
.accessibilityLabel(Text("tour.common.skip"))
Text(verbatim: String.localizedStringWithFormat(
String(localized: "tour.common.stepCounter %lld %lld"),
String(localized: "%lld von %lld"),
Int64(currentIndex + 1),
Int64(totalSteps)
))
@@ -78,18 +77,19 @@ struct TourCardView: View {
Button {
coordinator.previous()
} label: {
Text("tour.common.back")
Text("Zurück")
.font(.subheadline.weight(.medium))
.foregroundStyle(.secondary)
}
.padding(.trailing, 8)
}
// Next / Finish button
// Next / Finish button explicit LocalizedStringKey to ensure lookup
let nextLabel: LocalizedStringKey = coordinator.isLastStep ? "Loslegen" : "Weiter"
Button {
coordinator.next()
} label: {
Text(coordinator.isLastStep ? "tour.common.finish" : "tour.common.next")
Text(nextLabel)
.font(.subheadline.weight(.semibold))
.foregroundStyle(.white)
.padding(.horizontal, 18)
+14 -13
View File
@@ -3,6 +3,7 @@ import Foundation
// MARK: - TourCatalog
/// 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`.
enum TourCatalog {
@@ -10,41 +11,41 @@ enum TourCatalog {
static let onboarding = Tour(
id: .onboarding,
title: "tour.onboarding.title",
title: "App-Einführung",
steps: [
TourStep(
title: "tour.onboarding.step1.title",
body: "tour.onboarding.step1.body",
title: "Willkommen bei nahbar",
body: "nahbar hilft dir, echte Verbindungen zu pflegen ohne Stress, ohne Algorithmen.",
target: nil,
preferredCardPosition: .center
),
TourStep(
title: "tour.onboarding.step2.title",
body: "tour.onboarding.step2.body",
title: "Deine Menschen im Mittelpunkt",
body: "Füge Personen hinzu, die dir wichtig sind. Notiere Interessen, Gesprächsthemen und was euch verbindet.",
target: nil,
preferredCardPosition: .center
),
TourStep(
title: "tour.onboarding.step3.title",
body: "tour.onboarding.step3.body",
title: "Momente festhalten",
body: "Erfasse Treffen, Nachrichten und Erlebnisse. So weißt du, worüber ihr das letzte Mal geredet habt.",
target: nil,
preferredCardPosition: .center
),
TourStep(
title: "tour.onboarding.step4.title",
body: "tour.onboarding.step4.body",
title: "Plane das Nächste",
body: "Leg Vorhaben an und erhalte eine Erinnerung damit aus 'Wir müssen mal wieder…' ein echtes Treffen wird.",
target: nil,
preferredCardPosition: .center
),
TourStep(
title: "tour.onboarding.step5.title",
body: "tour.onboarding.step5.body",
title: "Sanfte Erinnerungen",
body: "nahbar erinnert dich, wenn du lange nichts von jemandem gehört hast. Du entscheidest, wie oft.",
target: nil,
preferredCardPosition: .center
),
TourStep(
title: "tour.onboarding.step6.title",
body: "tour.onboarding.step6.body",
title: "Einblicke, wenn du willst",
body: "Optionale KI-Analyse zeigt Muster in deinen Verbindungen. Alles optional deine Daten bleiben bei dir.",
target: nil,
preferredCardPosition: .center
),
+1 -1
View File
@@ -483,7 +483,7 @@ struct InterestTagHelperSuggestionsTests {
@Test("Duplikate werden dedupliziert")
func deduplicates() {
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)
}
}
@@ -1,3 +1,4 @@
import Foundation
import Testing
@testable import nahbar
@@ -1,5 +1,6 @@
import Testing
import CoreGraphics
import Foundation
import Testing
@testable import nahbar
// MARK: - TourCoordinator Tests