From 0d91c82bb14eabababe5dfbe3feaf3867cde08c1 Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Thu, 13 Aug 2026 10:57:36 +0100 Subject: [PATCH] Localize all app copy via Localizable.strings --- FuelBoard/ContentView.swift | 2 +- FuelBoard/OnboardingView.swift | 6 +- FuelBoard/SettingsView.swift | 26 ++--- FuelBoard/en.lproj/Localizable.strings | 127 +++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 17 deletions(-) create mode 100644 FuelBoard/en.lproj/Localizable.strings diff --git a/FuelBoard/ContentView.swift b/FuelBoard/ContentView.swift index 6e35fd5..524cab8 100644 --- a/FuelBoard/ContentView.swift +++ b/FuelBoard/ContentView.swift @@ -420,7 +420,7 @@ struct StationRow: View { private var deltaText: String? { guard let price = station.prices[fuel], let baselinePrice else { return nil } let delta = price - baselinePrice - if abs(delta) <= 0.05 { return "best" } + if abs(delta) <= 0.05 { return NSLocalizedString("best", comment: "") } // Signed: +X.Xp pricier than baseline, -X.Xp cheaper (Closest mode); // in Cheapest mode the baseline is the cheapest so deltas are ≥ 0. return String(format: "%+.1fp", delta) diff --git a/FuelBoard/OnboardingView.swift b/FuelBoard/OnboardingView.swift index 6ee24af..043ad29 100644 --- a/FuelBoard/OnboardingView.swift +++ b/FuelBoard/OnboardingView.swift @@ -324,7 +324,7 @@ struct OnboardingView: View { } } - private func primaryButton(_ title: String, action: @escaping () -> Void) -> some View { + private func primaryButton(_ title: LocalizedStringKey, action: @escaping () -> Void) -> some View { Button(action: action) { Text(title) .font(.headline) @@ -334,7 +334,7 @@ struct OnboardingView: View { .buttonStyle(.borderedProminent) } - private func featureRow(icon: String, text: String) -> some View { + private func featureRow(icon: String, text: LocalizedStringKey) -> some View { HStack(spacing: 12) { Image(systemName: icon) .font(.system(size: 16, weight: .semibold)) @@ -348,7 +348,7 @@ struct OnboardingView: View { .padding(.vertical, 5) } - private func permissionStatusLabel(granted: Bool, denied: Bool, deniedText: String) -> some View { + private func permissionStatusLabel(granted: Bool, denied: Bool, deniedText: LocalizedStringKey) -> some View { Group { if granted { Label("Allowed", systemImage: "checkmark.circle.fill") diff --git a/FuelBoard/SettingsView.swift b/FuelBoard/SettingsView.swift index fe4b32c..4df1a37 100644 --- a/FuelBoard/SettingsView.swift +++ b/FuelBoard/SettingsView.swift @@ -272,12 +272,12 @@ struct SettingsView: View { } private func inRangeText(_ status: DebugLocationStatus) -> String { - guard status.coordinate != nil else { return "waiting for fix" } + guard status.coordinate != nil else { return NSLocalizedString("waiting for fix", comment: "") } let fuelName = status.fuel.displayName.lowercased() if status.inRangeCount == 1 { - return "1 \(fuelName) station within \(distanceUnit.format(status.radiusKM))" + return String(format: NSLocalizedString("1 %@ station within %@", comment: ""), fuelName, distanceUnit.format(status.radiusKM)) } - return "\(status.inRangeCount) \(fuelName) stations within \(distanceUnit.format(status.radiusKM))" + return String(format: NSLocalizedString("%lld %@ stations within %@", comment: ""), status.inRangeCount, fuelName, distanceUnit.format(status.radiusKM)) } private func cheapestText(_ station: FuelStation, fuel: FuelType, distance: Double?) -> String { @@ -289,9 +289,9 @@ struct SettingsView: View { } private func ageText(_ age: TimeInterval) -> String { - if age < 60 { return "\(Int(age))s ago" } - if age < 3600 { return "\(Int(age / 60))m \(Int(age.truncatingRemainder(dividingBy: 60)))s ago" } - return "\(Int(age / 3600))h \(Int((age.truncatingRemainder(dividingBy: 3600)) / 60))m ago" + if age < 60 { return String(format: NSLocalizedString("%llds ago", comment: ""), Int(age)) } + if age < 3600 { return String(format: NSLocalizedString("%lldm %llds ago", comment: ""), Int(age / 60), Int(age.truncatingRemainder(dividingBy: 60))) } + return String(format: NSLocalizedString("%lldh %lldm ago", comment: ""), Int(age / 3600), Int((age.truncatingRemainder(dividingBy: 3600)) / 60)) } private var appVersion: String { @@ -358,7 +358,7 @@ struct SettingsView: View { case .authorized, .provisional, .ephemeral: onTestAlert() case .denied: - testAlertMessage = "Notifications are turned off for FuelBoard. Enable them in Settings → Notifications → FuelBoard, then try again." + testAlertMessage = NSLocalizedString("Notifications are turned off for FuelBoard. Enable them in Settings → Notifications → FuelBoard, then try again.", comment: "") default: // First time — ask, then fire if granted. UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in @@ -366,7 +366,7 @@ struct SettingsView: View { if granted { onTestAlert() } else { - testAlertMessage = "Notifications weren't allowed, so no test alert was sent." + testAlertMessage = NSLocalizedString("Notifications weren't allowed, so no test alert was sent.", comment: "") } } } @@ -412,7 +412,7 @@ final class TipStore: ObservableObject { // Connect yet), still allow the attempt so the user sees a clear // outcome rather than a dead button. guard let product else { - message = "The tip isn't available in this build yet — check back after an App Store release." + message = NSLocalizedString("The tip isn't available in this build yet — check back after an App Store release.", comment: "") return } @@ -422,19 +422,19 @@ final class TipStore: ObservableObject { case .success(let verification): switch verification { case .verified: - message = "Thank you! Your tip has been received. ⛽" + message = NSLocalizedString("Thank you! Your tip has been received. ⛽", comment: "") case .unverified: - message = "The purchase couldn't be verified. Please try again." + message = NSLocalizedString("The purchase couldn't be verified. Please try again.", comment: "") } case .userCancelled: message = nil // silent — the user just closed the sheet case .pending: - message = "Your tip is pending approval. It'll finish automatically." + message = NSLocalizedString("Your tip is pending approval. It'll finish automatically.", comment: "") @unknown default: message = nil } } catch { - message = "The tip couldn't be completed: \(error.localizedDescription)" + message = String(format: NSLocalizedString("The tip couldn't be completed: %@", comment: ""), error.localizedDescription) } } } diff --git a/FuelBoard/en.lproj/Localizable.strings b/FuelBoard/en.lproj/Localizable.strings new file mode 100644 index 0000000..1d147d2 --- /dev/null +++ b/FuelBoard/en.lproj/Localizable.strings @@ -0,0 +1,127 @@ +/* FuelBoard — app copy. Edit the VALUE (right side) to change wording. + Keys are the English source strings, so a missing key falls back to + English instead of showing a raw key. Interpolated values use + %@ (String), %lld (Int). */ + +/* Tabs & navigation */ +"Stations" = "Stations"; +"Favourites" = "Favourites"; +"Alerts" = "Alerts"; +"Settings" = "Settings"; +"FuelBoard" = "FuelBoard"; +"TOP" = "TOP"; + +/* Stations tab */ +"Closest %@ stations — nearest first, best value within %lld %@ · %lld stations" = "Closest %@ stations — nearest first, best value within %lld %@ · %lld stations"; +"Cheapest %@ within %lld %@ — %lld stations · tap a station for directions." = "Cheapest %@ within %lld %@ — %lld stations · tap a station for directions."; +"%@ %@ — tap a station for directions." = "%@ %@ — tap a station for directions."; +"Fuel type" = "Fuel type"; +"Distance" = "Distance"; +"Sort by" = "Sort by"; +"Fetching prices…" = "Fetching prices…"; +"No %@ stations found." = "No %@ stations found."; +"Show %lld more (%lld remaining)" = "Show %lld more (%lld remaining)"; +"All %lld stations shown" = "All %lld stations shown"; +"Key" = "Key"; +"Best value — within 1.5p of the cheapest" = "Best value — within 1.5p of the cheapest"; +"Okay — within 4p of the cheapest" = "Okay — within 4p of the cheapest"; +"Pricey — more than 4p over the cheapest" = "Pricey — more than 4p over the cheapest"; +"Top result for the current sort" = "Top result for the current sort"; +"Star a station to add it to Favourites" = "Star a station to add it to Favourites"; + +/* Favourites tab */ +"No favourites yet" = "No favourites yet"; +"Tap the star on any station in the Stations tab to pin it here, ranked by which is cheapest for that fuel." = "Tap the star on any station in the Stations tab to pin it here, ranked by which is cheapest for that fuel."; +"%@ favourites — cheapest first" = "%@ favourites — cheapest first"; +"%@ is your cheapest %@ favourite. Favourites are monitored automatically — switch to the Alerts tab and enable alerts to get pinged when you approach one that's the cheapest within the radius." = "%@ is your cheapest %@ favourite. Favourites are monitored automatically — switch to the Alerts tab and enable alerts to get pinged when you approach one that's the cheapest within the radius."; + +/* Alerts tab */ +"Cheapest-station alerts" = "Cheapest-station alerts"; +"Fuel" = "Fuel"; +"Radius" = "Radius"; +"%lld %@" = "%lld %@"; +"When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Alerts watch for the cheapest %@ station within the radius." = "When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Alerts watch for the cheapest %@ station within the radius."; +"Monitoring" = "Monitoring"; +"No stations monitored yet — open the Stations tab to load prices first." = "No stations monitored yet — open the Stations tab to load prices first."; +"Geofenced stations" = "Geofenced stations"; +"Your favourites for %@ get priority, then the closest stations selling that fuel fill the rest (18 max, iOS region limit)." = "Your favourites for %@ get priority, then the closest stations selling that fuel fill the rest (18 max, iOS region limit)."; +"Alerts are checked against the cheapest station within the trigger radius for the fuel chosen above. Each station alerts at most once per hour." = "Alerts are checked against the cheapest station within the trigger radius for the fuel chosen above. Each station alerts at most once per hour."; +"Last alert" = "Last alert"; +"Live Activity" = "Live Activity"; +"Shows the cheapest station for the fuel and distance you pick here on the Lock Screen and Dynamic Island — independent of the Stations tab. Updates as you drive; tap to open directions. Live Activities can be disabled in System Settings → Live Activities." = "Shows the cheapest station for the fuel and distance you pick here on the Lock Screen and Dynamic Island — independent of the Stations tab. Updates as you drive; tap to open directions. Live Activities can be disabled in System Settings → Live Activities."; + +/* Settings tab */ +"Units" = "Units"; +"Distances and search radii across the app, widget and alerts are shown in this unit." = "Distances and search radii across the app, widget and alerts are shown in this unit."; +"Show introduction" = "Show introduction"; +"Replay the welcome screen, including the location and notification permission prompts." = "Replay the welcome screen, including the location and notification permission prompts."; +"Test alert notification (real data)" = "Test alert notification (real data)"; +"Send plain test notification" = "Send plain test notification"; +"Debug" = "Debug"; +"The first button applies the real criteria — cheapest %@ station within %@ of you — and sends the actual station's name, price and coordinates. The second fires with no criteria at all (nearest seller), still carrying a real station, just to verify a notification appears and tapping it opens directions. Cheapest in range mirrors the app list (selected fuel + distance); the in-range count mirrors the alert radius." = "The first button applies the real criteria — cheapest %@ station within %@ of you — and sends the actual station's name, price and coordinates. The second fires with no criteria at all (nearest seller), still carrying a real station, just to verify a notification appears and tapping it opens directions. Cheapest in range mirrors the app list (selected fuel + distance); the in-range count mirrors the alert radius."; +"Leave a tip" = "Leave a tip"; +"Support FuelBoard" = "Support FuelBoard"; +"A small tip helps keep the data relay and app development going. Thank you!" = "A small tip helps keep the data relay and app development going. Thank you!"; +"Connection" = "Connection"; +"Data updated" = "Data updated"; +"Data source" = "Data source"; +"Connection shows whether prices come from the official Fuel Finder API or the CSV mirror. Data updated is the latest price change reported by the GOV.UK server itself." = "Connection shows whether prices come from the official Fuel Finder API or the CSV mirror. Data updated is the latest price change reported by the GOV.UK server itself."; +"Version" = "Version"; +"About" = "About"; +"Device" = "Device"; +"No fix" = "No fix"; +"Fix age" = "Fix age"; +"In range" = "In range"; +"Cheapest in range" = "Cheapest in range"; +"waiting for fix" = "waiting for fix"; +"1 %@ station within %@" = "1 %@ station within %@"; +"%lld %@ stations within %@" = "%lld %@ stations within %@"; +"%llds ago" = "%llds ago"; +"%lldm %llds ago" = "%lldm %llds ago"; +"%lldh %lldm ago" = "%lldh %lldm ago"; +"Notifications are turned off for FuelBoard. Enable them in Settings → Notifications → FuelBoard, then try again." = "Notifications are turned off for FuelBoard. Enable them in Settings → Notifications → FuelBoard, then try again."; +"Notifications weren't allowed, so no test alert was sent." = "Notifications weren't allowed, so no test alert was sent."; +"The tip isn't available in this build yet — check back after an App Store release." = "The tip isn't available in this build yet — check back after an App Store release."; +"Thank you! Your tip has been received. ⛽" = "Thank you! Your tip has been received. ⛽"; +"The purchase couldn't be verified. Please try again." = "The purchase couldn't be verified. Please try again."; +"Your tip is pending approval. It'll finish automatically." = "Your tip is pending approval. It'll finish automatically."; +"The tip couldn't be completed: %@" = "The tip couldn't be completed: %@"; + +/* Onboarding */ +"Welcome to FuelBoard" = "Welcome to FuelBoard"; +"The cheapest petrol, diesel and premium fuel near you — from the official UK Fuel Finder data." = "The cheapest petrol, diesel and premium fuel near you — from the official UK Fuel Finder data."; +"England-wide prices — 8,000+ stations, updated twice a day" = "England-wide prices — 8,000+ stations, updated twice a day"; +"Cheapest within your chosen radius, or closest station first" = "Cheapest within your chosen radius, or closest station first"; +"Favourites with instant price comparison" = "Favourites with instant price comparison"; +"Green/amber/red rating vs the best nearby price" = "Green/amber/red rating vs the best nearby price"; +"Home-screen widget showing the cheapest nearby" = "Home-screen widget showing the cheapest nearby"; +"Alerts when you approach the cheapest station" = "Alerts when you approach the cheapest station"; +"Prices near you" = "Prices near you"; +"FuelBoard uses your location to show prices around you and rank stations by distance. Your position never leaves the device." = "FuelBoard uses your location to show prices around you and rank stations by distance. Your position never leaves the device."; +"Location was denied. You can still browse all stations, but \"nearest\" sorting needs it." = "Location was denied. You can still browse all stations, but \"nearest\" sorting needs it."; +"Allowed" = "Allowed"; +"The system prompt will appear next." = "The system prompt will appear next."; +"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." = "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."; +"Notifications were denied — you can still use FuelBoard, just without alert banners." = "Notifications were denied — you can still use FuelBoard, just without alert banners."; +"Prices, ready when you are" = "Prices, ready when you are"; +"FuelBoard downloads the latest prices from a relay on your local network — the full UK dataset, refreshed twice a day. Local network access is needed for that first download." = "FuelBoard downloads the latest prices from a relay on your local network — the full UK dataset, refreshed twice a day. Local network access is needed for that first download."; +"Waiting for network permission…" = "Waiting for network permission…"; +"Connected" = "Connected"; +"Local network access was denied — prices won't load until it's allowed, but you can keep using FuelBoard." = "Local network access was denied — prices won't load until it's allowed, but you can keep using FuelBoard."; +"You're all set" = "You're all set"; +"Find your cheapest fuel, add favourites, drop the widget on your Home Screen, and let alerts point you to the best price nearby." = "Find your cheapest fuel, add favourites, drop the widget on your Home Screen, and let alerts point you to the best price nearby."; +"Continue" = "Continue"; +"Open Settings" = "Open Settings"; +"Allow Location Access" = "Allow Location Access"; +"Continue without alerts" = "Continue without alerts"; +"Allow Notifications" = "Allow Notifications"; +"Waiting…" = "Waiting…"; +"Start Using FuelBoard" = "Start Using FuelBoard"; + +/* Station map */ +"You" = "You"; +"Done" = "Done"; +"Directions" = "Directions"; + +/* Station row */ +"best" = "best";