Localize all app copy via Localizable.strings
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
Reference in New Issue
Block a user