Localize all app copy via Localizable.strings

This commit is contained in:
FuelBoard Contributor
2026-08-13 10:57:36 +01:00
parent bbe43a6149
commit 0d91c82bb1
4 changed files with 144 additions and 17 deletions
+1 -1
View File
@@ -420,7 +420,7 @@ struct StationRow: View {
private var deltaText: String? { private var deltaText: String? {
guard let price = station.prices[fuel], let baselinePrice else { return nil } guard let price = station.prices[fuel], let baselinePrice else { return nil }
let delta = price - baselinePrice 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); // Signed: +X.Xp pricier than baseline, -X.Xp cheaper (Closest mode);
// in Cheapest mode the baseline is the cheapest so deltas are 0. // in Cheapest mode the baseline is the cheapest so deltas are 0.
return String(format: "%+.1fp", delta) return String(format: "%+.1fp", delta)
+3 -3
View File
@@ -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) { Button(action: action) {
Text(title) Text(title)
.font(.headline) .font(.headline)
@@ -334,7 +334,7 @@ struct OnboardingView: View {
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
} }
private func featureRow(icon: String, text: String) -> some View { private func featureRow(icon: String, text: LocalizedStringKey) -> some View {
HStack(spacing: 12) { HStack(spacing: 12) {
Image(systemName: icon) Image(systemName: icon)
.font(.system(size: 16, weight: .semibold)) .font(.system(size: 16, weight: .semibold))
@@ -348,7 +348,7 @@ struct OnboardingView: View {
.padding(.vertical, 5) .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 { Group {
if granted { if granted {
Label("Allowed", systemImage: "checkmark.circle.fill") Label("Allowed", systemImage: "checkmark.circle.fill")
+13 -13
View File
@@ -272,12 +272,12 @@ struct SettingsView: View {
} }
private func inRangeText(_ status: DebugLocationStatus) -> String { 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() let fuelName = status.fuel.displayName.lowercased()
if status.inRangeCount == 1 { 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 { private func cheapestText(_ station: FuelStation, fuel: FuelType, distance: Double?) -> String {
@@ -289,9 +289,9 @@ struct SettingsView: View {
} }
private func ageText(_ age: TimeInterval) -> String { private func ageText(_ age: TimeInterval) -> String {
if age < 60 { return "\(Int(age))s ago" } if age < 60 { return String(format: NSLocalizedString("%llds ago", comment: ""), Int(age)) }
if age < 3600 { return "\(Int(age / 60))m \(Int(age.truncatingRemainder(dividingBy: 60)))s ago" } if age < 3600 { return String(format: NSLocalizedString("%lldm %llds ago", comment: ""), Int(age / 60), Int(age.truncatingRemainder(dividingBy: 60))) }
return "\(Int(age / 3600))h \(Int((age.truncatingRemainder(dividingBy: 3600)) / 60))m ago" return String(format: NSLocalizedString("%lldh %lldm ago", comment: ""), Int(age / 3600), Int((age.truncatingRemainder(dividingBy: 3600)) / 60))
} }
private var appVersion: String { private var appVersion: String {
@@ -358,7 +358,7 @@ struct SettingsView: View {
case .authorized, .provisional, .ephemeral: case .authorized, .provisional, .ephemeral:
onTestAlert() onTestAlert()
case .denied: 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: default:
// First time ask, then fire if granted. // First time ask, then fire if granted.
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
@@ -366,7 +366,7 @@ struct SettingsView: View {
if granted { if granted {
onTestAlert() onTestAlert()
} else { } 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 // Connect yet), still allow the attempt so the user sees a clear
// outcome rather than a dead button. // outcome rather than a dead button.
guard let product else { 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 return
} }
@@ -422,19 +422,19 @@ final class TipStore: ObservableObject {
case .success(let verification): case .success(let verification):
switch verification { switch verification {
case .verified: case .verified:
message = "Thank you! Your tip has been received. ⛽" message = NSLocalizedString("Thank you! Your tip has been received. ⛽", comment: "")
case .unverified: 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: case .userCancelled:
message = nil // silent the user just closed the sheet message = nil // silent the user just closed the sheet
case .pending: 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: @unknown default:
message = nil message = nil
} }
} catch { } catch {
message = "The tip couldn't be completed: \(error.localizedDescription)" message = String(format: NSLocalizedString("The tip couldn't be completed: %@", comment: ""), error.localizedDescription)
} }
} }
} }
+127
View File
@@ -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";