Localize onboarding copy

This commit is contained in:
FuelBoard Contributor
2026-10-01 12:54:43 +01:00
parent 50c543c776
commit 824923b2ac
+31 -30
View File
@@ -4,8 +4,9 @@ import UserNotifications
import Network import Network
/// First-launch onboarding: introduces FuelBoard, then walks the user through /// First-launch onboarding: introduces FuelBoard, then walks the user through
/// the three system permissions (location, notifications, local network for /// the two system permissions (location and notifications) with in-context
/// data loading) with in-context prompts. In production it appears once at /// prompts. Price data downloads over HTTPS with no Local Network permission.
/// In production it appears once at
/// initial launch (driven by `FuelStore.loadHasCompletedOnboarding()`); a /// initial launch (driven by `FuelStore.loadHasCompletedOnboarding()`); a
/// test button in the Alerts tab re-opens it anytime. /// test button in the Alerts tab re-opens it anytime.
struct OnboardingView: View { struct OnboardingView: View {
@@ -97,23 +98,23 @@ struct OnboardingView: View {
} }
.padding(.bottom, 28) .padding(.bottom, 28)
Text("Welcome to FuelBoard") Text(String(localized: "Welcome to FuelBoard", comment: "Onboarding welcome title"))
.font(.largeTitle.bold()) .font(.largeTitle.bold())
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
Text("The cheapest petrol, diesel and premium fuel near you — from the official UK Fuel Finder data.") Text(String(localized: "The cheapest petrol, diesel and premium fuel near you — from the official UK Fuel Finder data.", comment: "Onboarding welcome subtitle"))
.font(.body) .font(.body)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.horizontal, 32) .padding(.horizontal, 32)
.padding(.top, 12) .padding(.top, 12)
featureRow(icon: "globe.europe.africa.fill", text: "UK-wide prices — 8,000+ stations, updated twice a day") featureRow(icon: "globe.europe.africa.fill", text: String(localized: "UK-wide prices — 8,000+ stations, updated twice a day", comment: "Onboarding feature row"))
featureRow(icon: "scope", text: "Cheapest within your chosen radius, or closest station first") featureRow(icon: "scope", text: String(localized: "Cheapest within your chosen radius, or closest station first", comment: "Onboarding feature row"))
featureRow(icon: "star.fill", text: "Favourites with instant price comparison") featureRow(icon: "star.fill", text: String(localized: "Favourites with instant price comparison", comment: "Onboarding feature row"))
featureRow(icon: "chart.bar.fill", text: "Green/amber/red rating vs the best nearby price") featureRow(icon: "chart.bar.fill", text: String(localized: "Green/amber/red rating vs the best nearby price", comment: "Onboarding feature row"))
featureRow(icon: "square.grid.2x2.fill", text: "Home-screen widget showing the cheapest nearby") featureRow(icon: "square.grid.2x2.fill", text: String(localized: "Home-screen widget showing the cheapest nearby", comment: "Onboarding feature row"))
featureRow(icon: "bell.fill", text: "Alerts when you approach the cheapest station") featureRow(icon: "bell.fill", text: String(localized: "Alerts when you approach the cheapest station", comment: "Onboarding feature row"))
Spacer() Spacer()
} }
@@ -129,10 +130,10 @@ struct OnboardingView: View {
} }
.padding(.bottom, 28) .padding(.bottom, 28)
Text("Prices near you") Text(String(localized: "Prices near you", comment: "Onboarding location permission title"))
.font(.largeTitle.bold()) .font(.largeTitle.bold())
Text("FuelBoard uses your location to show prices around you and rank stations by distance. Your position never leaves the device.") Text(String(localized: "FuelBoard uses your location to show prices around you and rank stations by distance. Your position never leaves the device.", comment: "Onboarding location permission explanation"))
.font(.body) .font(.body)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@@ -142,7 +143,7 @@ struct OnboardingView: View {
permissionStatusLabel( permissionStatusLabel(
granted: prompter.locationGranted, granted: prompter.locationGranted,
denied: prompter.locationDenied, denied: prompter.locationDenied,
deniedText: "Location was denied. You can still browse all stations, but \"nearest\" sorting needs it." deniedText: String(localized: "Location was denied. You can still browse all stations, but \"nearest\" sorting needs it.", comment: "Onboarding location denied text")
) )
.padding(.top, 24) .padding(.top, 24)
@@ -160,10 +161,10 @@ struct OnboardingView: View {
} }
.padding(.bottom, 28) .padding(.bottom, 28)
Text("Cheapest-station alerts") Text(String(localized: "Cheapest-station alerts", comment: "Onboarding notifications title"))
.font(.largeTitle.bold()) .font(.largeTitle.bold())
Text("FuelBoard can notify you when you're approaching the cheapest station in your alert radius — even with the app closed. You can switch this off in the Alerts tab.") Text(String(localized: "FuelBoard can notify you when you're approaching the cheapest station in your alert radius — even with the app closed. You can switch this off in the Alerts tab.", comment: "Onboarding notifications explanation"))
.font(.body) .font(.body)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@@ -173,7 +174,7 @@ struct OnboardingView: View {
permissionStatusLabel( permissionStatusLabel(
granted: prompter.notificationsGranted, granted: prompter.notificationsGranted,
denied: prompter.notificationsDenied, denied: prompter.notificationsDenied,
deniedText: "Notifications were denied — you can still use FuelBoard, just without alert banners." deniedText: String(localized: "Notifications were denied — you can still use FuelBoard, just without alert banners.", comment: "Onboarding notifications denied text")
) )
.padding(.top, 24) .padding(.top, 24)
@@ -191,10 +192,10 @@ struct OnboardingView: View {
} }
.padding(.bottom, 28) .padding(.bottom, 28)
Text("Prices, ready when you are") Text(String(localized: "Prices, ready when you are", comment: "Onboarding data title"))
.font(.largeTitle.bold()) .font(.largeTitle.bold())
Text("FuelBoard downloads the latest prices from the internet automatically — the full UK dataset, refreshed twice a day. No extra permissions needed.") Text(String(localized: "FuelBoard downloads the latest prices from the internet automatically — the full UK dataset, refreshed twice a day. No extra permissions needed.", comment: "Onboarding data explanation"))
.font(.body) .font(.body)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@@ -218,10 +219,10 @@ struct OnboardingView: View {
} }
.padding(.bottom, 28) .padding(.bottom, 28)
Text("You're all set") Text(String(localized: "You're all set", comment: "Onboarding final title"))
.font(.largeTitle.bold()) .font(.largeTitle.bold())
Text("Find your cheapest fuel, add favourites, drop the widget on your Home Screen, and let alerts point you to the best price nearby.") Text(String(localized: "Find your cheapest fuel, add favourites, drop the widget on your Home Screen, and let alerts point you to the best price nearby.", comment: "Onboarding final explanation"))
.font(.body) .font(.body)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@@ -238,10 +239,10 @@ struct OnboardingView: View {
private var bottomAction: some View { private var bottomAction: some View {
switch page { switch page {
case 0: case 0:
primaryButton("Continue") { page = 1 } primaryButton(String(localized: "Continue", comment: "Onboarding continue button")) { page = 1 }
case 1: case 1:
primaryButton( primaryButton(
prompter.locationDenied ? "Open Settings" : (prompter.locationGranted ? "Continue" : "Allow Location Access") prompter.locationDenied ? String(localized: "Open Settings", comment: "Open iOS Settings button") : (prompter.locationGranted ? String(localized: "Continue", comment: "Onboarding continue button") : String(localized: "Allow Location Access", comment: "Request location permission button"))
) { ) {
if prompter.locationDenied { if prompter.locationDenied {
openSettings() openSettings()
@@ -258,7 +259,7 @@ struct OnboardingView: View {
} }
case 2: case 2:
primaryButton( primaryButton(
prompter.notificationsDenied ? "Continue without alerts" : (prompter.notificationsGranted ? "Continue" : "Allow Notifications") prompter.notificationsDenied ? String(localized: "Continue without alerts", comment: "Continue after notification permission denied") : (prompter.notificationsGranted ? String(localized: "Continue", comment: "Onboarding continue button") : String(localized: "Allow Notifications", comment: "Request notifications permission button"))
) { ) {
if prompter.notificationsDenied || prompter.notificationsGranted { if prompter.notificationsDenied || prompter.notificationsGranted {
page = 3 // decided either way — move on page = 3 // decided either way — move on
@@ -269,13 +270,13 @@ struct OnboardingView: View {
} }
case 3: case 3:
// No permission on this page — prices need none. Straight on. // No permission on this page — prices need none. Straight on.
primaryButton("Continue") { page = 4 } primaryButton(String(localized: "Continue", comment: "Onboarding continue button")) { page = 4 }
default: default:
primaryButton("Start Using FuelBoard") { finish() } primaryButton(String(localized: "Start Using FuelBoard", comment: "Finish onboarding button")) { finish() }
} }
} }
private func primaryButton(_ title: LocalizedStringKey, action: @escaping () -> Void) -> some View { private func primaryButton(_ title: String, action: @escaping () -> Void) -> some View {
Button(action: action) { Button(action: action) {
Text(title) Text(title)
.font(.headline) .font(.headline)
@@ -285,7 +286,7 @@ struct OnboardingView: View {
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
} }
private func featureRow(icon: String, text: LocalizedStringKey) -> some View { private func featureRow(icon: String, text: String) -> some View {
HStack(spacing: 12) { HStack(spacing: 12) {
Image(systemName: icon) Image(systemName: icon)
.font(.system(size: 20, weight: .semibold)) .font(.system(size: 20, weight: .semibold))
@@ -299,10 +300,10 @@ struct OnboardingView: View {
.padding(.vertical, 10) .padding(.vertical, 10)
} }
private func permissionStatusLabel(granted: Bool, denied: Bool, deniedText: LocalizedStringKey) -> some View { private func permissionStatusLabel(granted: Bool, denied: Bool, deniedText: String) -> some View {
Group { Group {
if granted { if granted {
Label("Allowed", systemImage: "checkmark.circle.fill") Label(String(localized: "Allowed", comment: "Permission granted status"), systemImage: "checkmark.circle.fill")
.foregroundStyle(.green) .foregroundStyle(.green)
} else if denied { } else if denied {
Text(deniedText) Text(deniedText)
@@ -311,7 +312,7 @@ struct OnboardingView: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.horizontal, 32) .padding(.horizontal, 32)
} else { } else {
Text("The system prompt will appear next.") Text(String(localized: "The system prompt will appear next.", comment: "Permission prompt pending status"))
.font(.footnote) .font(.footnote)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }