Localize alerts and favourites copy

This commit is contained in:
FuelBoard Contributor
2026-10-01 15:25:08 +01:00
parent 824923b2ac
commit 13d84eb32d
3 changed files with 55 additions and 39 deletions
+31 -31
View File
@@ -38,7 +38,7 @@ struct AlertsView: View {
/// "5 miles" or "8 km" — for the "Follow search" rows. /// "5 miles" or "8 km" — for the "Follow search" rows.
private var searchInUnitLabel: String { private var searchInUnitLabel: String {
let shown = distanceUnit.displayMiles(stationLimit) // stationLimit is always miles let shown = distanceUnit.displayMiles(stationLimit) // stationLimit is always miles
return "\(shown) \(distanceUnit.label(for: Double(shown)))" return String(format: String(localized: "%lld %@", comment: "Distance label"), shown, distanceUnit.label(for: Double(shown)))
} }
/// A miles value (5/10/15) shown truthfully in the user's unit, e.g. /// A miles value (5/10/15) shown truthfully in the user's unit, e.g.
@@ -46,7 +46,7 @@ struct AlertsView: View {
/// what the option really means. /// what the option really means.
private func distanceLabel(forMiles miles: Int) -> String { private func distanceLabel(forMiles miles: Int) -> String {
let shown = distanceUnit.displayMiles(miles) let shown = distanceUnit.displayMiles(miles)
return "\(shown) \(distanceUnit.label(for: Double(shown)))" return String(format: String(localized: "%lld %@", comment: "Distance label"), shown, distanceUnit.label(for: Double(shown)))
} }
/// Situational guidance for the currently-selected alert radius, shown /// Situational guidance for the currently-selected alert radius, shown
@@ -57,23 +57,23 @@ struct AlertsView: View {
private var alertRadiusGuidance: String { private var alertRadiusGuidance: String {
switch alertChoice.wrappedValue { switch alertChoice.wrappedValue {
case .followSearch: case .followSearch:
return "Set-and-forget: alerts behave exactly like the Stations-tab search distance (capped at 8 miles for reliable geofencing)." return String(localized: "Set-and-forget: alerts behave exactly like the Stations-tab search distance (capped at 8 miles for reliable geofencing).", comment: "Alert radius guidance for following search distance")
case .fixed(let value): case .fixed(let value):
let optionLabel = "\(value) \(distanceUnit.label(for: Double(value)))" let optionLabel = String(format: String(localized: "%lld %@", comment: "Distance label"), value, distanceUnit.label(for: Double(value)))
let body: String let body: String
switch value { switch value {
case 1: case 1:
body = "Town driving. Fires most often — you're rarely inside the circle for long, so pings are meaningful. Best for stop-start local trips." body = String(localized: "Town driving. Fires most often — you're rarely inside the circle for long, so pings are meaningful. Best for stop-start local trips.", comment: "Alert radius guidance")
case 2: case 2:
body = "City or suburb commute. Slightly earlier heads-up than the smallest option; still fires regularly around town." body = String(localized: "City or suburb commute. Slightly earlier heads-up than the smallest option; still fires regularly around town.", comment: "Alert radius guidance")
case 3: case 3:
body = "Mixed driving. Middle ground: heads-up a few minutes out, still works in most towns." body = String(localized: "Mixed driving. Middle ground: heads-up a few minutes out, still works in most towns.", comment: "Alert radius guidance")
case 5: case 5:
body = "A-roads and motorway exits. Wide early warning for longer trips — but in town it rarely fires, because you're usually already inside the circle." body = String(localized: "A-roads and motorway exits. Wide early warning for longer trips — but in town it rarely fires, because you're usually already inside the circle.", comment: "Alert radius guidance")
default: default:
body = "Long-distance journeys. Maximum reach: a ping when a cheap station is coming up from far out. In town this essentially never fires." body = String(localized: "Long-distance journeys. Maximum reach: a ping when a cheap station is coming up from far out. In town this essentially never fires.", comment: "Alert radius guidance")
} }
return "\(optionLabel) — \(body)" return String(format: String(localized: "%@ — %@", comment: "Alert radius guidance with option label"), optionLabel, body)
} }
} }
@@ -134,74 +134,74 @@ struct AlertsView: View {
NavigationStack { NavigationStack {
List { List {
Section { Section {
Toggle("Cheapest favourite", isOn: $favouriteDropEnabled) Toggle(String(localized: "Cheapest favourite", comment: "Favourite price-drop alert toggle"), isOn: $favouriteDropEnabled)
Picker("Fuel", selection: $favouriteDropFuel) { Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $favouriteDropFuel) {
ForEach(FuelType.allCases) { fuel in ForEach(FuelType.allCases) { fuel in
Text(fuel.displayName).tag(fuel) Text(fuel.displayName).tag(fuel)
} }
} }
Button("Check now", action: onCheckFavouriteDropNow) Button(String(localized: "Check now", comment: "Manual favourite price-drop check button"), action: onCheckFavouriteDropNow)
.disabled(!favouriteDropEnabled) .disabled(!favouriteDropEnabled)
} header: { } header: {
Text("Price-drop alerts") Text(String(localized: "Price-drop alerts", comment: "Favourite price-drop alerts section title"))
} footer: { } footer: {
Text("Watches your cheapest saved favourite for the fuel you pick here. FuelBoard alerts when a different favourite becomes the cheapest, or when your current cheapest favourite drops by at least 1.0p. This is separate from the nearby-geofence alert, so you can monitor favourites without extra location noise.") Text(String(localized: "Watches your cheapest saved favourite for the fuel you pick here. FuelBoard alerts when a different favourite becomes the cheapest, or when your current cheapest favourite drops by at least 1.0p. This is separate from the nearby-geofence alert, so you can monitor favourites without extra location noise.", comment: "Favourite price-drop alerts section footer"))
} }
Section { Section {
Toggle("Cheapest station", isOn: $enabled) Toggle(String(localized: "Cheapest station", comment: "Proximity alert toggle"), isOn: $enabled)
Picker("Fuel", selection: $fuel) { Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $fuel) {
ForEach(FuelType.allCases) { fuel in ForEach(FuelType.allCases) { fuel in
Text(fuel.displayName).tag(fuel) Text(fuel.displayName).tag(fuel)
} }
} }
Picker("Radius", selection: alertChoice) { Picker(String(localized: "Radius", comment: "Alert radius picker title"), selection: alertChoice) {
Text("Follow search (\(searchInUnitLabel))").tag(AlertRadiusChoice.followSearch) Text(String(format: String(localized: "Follow search (%@)", comment: "Follow current station search distance option"), searchInUnitLabel)).tag(AlertRadiusChoice.followSearch)
ForEach(FuelStore.alertRadiusOptions, id: \.self) { value in ForEach(FuelStore.alertRadiusOptions, id: \.self) { value in
Text("\(value) \(distanceUnit.label(for: Double(value)))").tag(AlertRadiusChoice.fixed(value)) Text(String(format: String(localized: "%lld %@", comment: "Distance label"), value, distanceUnit.label(for: Double(value)))).tag(AlertRadiusChoice.fixed(value))
} }
} }
Text(alertRadiusGuidance) Text(alertRadiusGuidance)
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
if enabled && monitoredCount == 0 { if enabled && monitoredCount == 0 {
Text("No stations monitored yet — open the Stations tab to load prices first.") Text(String(localized: "No stations monitored yet — open the Stations tab to load prices first.", comment: "No monitored stations hint"))
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} header: { } header: {
Text("Proximity alerts") Text(String(localized: "Proximity alerts", comment: "Proximity alerts section title"))
} footer: { } footer: {
Text("When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling \(fuel.displayName.lowercased()) are watched — favourites get priority — and each station alerts at most once per hour. Follow search mirrors the Stations-tab distance, capped at 8 miles for reliable geofencing. Smaller radii ping more often but later; larger radii ping rarely in town but earlier from afar.") Text(String(format: String(localized: "When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling %@ are watched — favourites get priority — and each station alerts at most once per hour. Follow search mirrors the Stations-tab distance, capped at 8 miles for reliable geofencing. Smaller radii ping more often but later; larger radii ping rarely in town but earlier from afar.", comment: "Proximity alerts section footer"), fuel.displayName.lowercased()))
} }
if enabled, let lastAlert { if enabled, let lastAlert {
Section("Last alert") { Section(String(localized: "Last alert", comment: "Last alert section title")) {
Label(lastAlert, systemImage: "bell.badge.fill") Label(lastAlert, systemImage: "bell.badge.fill")
.font(.footnote) .font(.footnote)
} }
} }
Section { Section {
Toggle("Cheapest station", isOn: $liveActivityEnabled) Toggle(String(localized: "Cheapest station", comment: "Live Activity toggle"), isOn: $liveActivityEnabled)
Picker("Fuel", selection: $liveActivityFuel) { Picker(String(localized: "Fuel", comment: "Fuel picker title"), selection: $liveActivityFuel) {
ForEach(FuelType.allCases) { fuel in ForEach(FuelType.allCases) { fuel in
Text(fuel.displayName).tag(fuel) Text(fuel.displayName).tag(fuel)
} }
} }
Picker("Distance", selection: laChoice) { Picker(String(localized: "Distance", comment: "Distance picker title"), selection: laChoice) {
Text("Follow search (\(searchInUnitLabel))").tag(LADistanceChoice.followSearch) Text(String(format: String(localized: "Follow search (%@)", comment: "Follow current station search distance option"), searchInUnitLabel)).tag(LADistanceChoice.followSearch)
ForEach(FuelStore.stationRadiusOptions, id: \.self) { miles in ForEach(FuelStore.stationRadiusOptions, id: \.self) { miles in
Text(distanceLabel(forMiles: miles)).tag(LADistanceChoice.fixed(miles)) Text(distanceLabel(forMiles: miles)).tag(LADistanceChoice.fixed(miles))
} }
} }
} header: { } header: {
Text("Live Activity") Text(String(localized: "Live Activity", comment: "Live Activity section title"))
} footer: { } footer: {
Text("Shows the cheapest station for the fuel and distance you pick here on the Lock Screen and Dynamic Island — independent of the Stations tab (choose Follow search to mirror its distance instead). Updates as you drive; tap to open directions. Live Activities can be disabled in System Settings → Live Activities.") Text(String(localized: "Shows the cheapest station for the fuel and distance you pick here on the Lock Screen and Dynamic Island — independent of the Stations tab (choose Follow search to mirror its distance instead). Updates as you drive; tap to open directions. Live Activities can be disabled in System Settings → Live Activities.", comment: "Live Activity section footer"))
} }
} }
.navigationTitle("Alerts") .navigationTitle(String(localized: "Alerts", comment: "Alerts tab title"))
} }
} }
} }
+8 -8
View File
@@ -106,9 +106,9 @@ struct FavouritesView: View {
Image(systemName: "star") Image(systemName: "star")
.font(.system(size: 40)) .font(.system(size: 40))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Text("No favourites yet") Text(String(localized: "No favourites yet", comment: "Favourites empty-state title"))
.font(.headline) .font(.headline)
Text("Tap the star on any station in the Stations tab to pin it here. The first station in each list becomes the favourite used in single widgets — drag to put your pick first.") Text(String(localized: "Tap the star on any station in the Stations tab to pin it here. The first station in each list becomes the favourite used in single widgets — drag to put your pick first.", comment: "Favourites empty-state explanation"))
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@@ -117,7 +117,7 @@ struct FavouritesView: View {
.padding(.vertical, 24) .padding(.vertical, 24)
} }
} else { } else {
Section("Fuel type") { Section(String(localized: "Fuel type", comment: "Fuel type section title")) {
FuelTypeSegmentedPicker( FuelTypeSegmentedPicker(
selection: $fuel, selection: $fuel,
fuels: availableFuels fuels: availableFuels
@@ -142,21 +142,21 @@ struct FavouritesView: View {
} }
.onMove(perform: moveFavourite) .onMove(perform: moveFavourite)
} header: { } header: {
Text("\(activeFuel.displayName) favourites") Text(String(format: String(localized: "%@ favourites", comment: "Favourites section title for fuel type"), activeFuel.displayName))
} footer: { } footer: {
Text("The first station is the favourite used in single widgets. Tap Edit, then drag to reorder.") Text(String(localized: "The first station is the favourite used in single widgets. Tap Edit, then drag to reorder.", comment: "Favourites reorder explanation"))
} }
if let cheapest = cheapest { if let cheapest = cheapest {
Section { Section {
Text("\(cheapest.name) is your cheapest \(activeFuel.displayName) 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.") Text(String(format: String(localized: "%@ 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.", comment: "Cheapest favourite explanation"), cheapest.name, activeFuel.displayName))
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
} }
} }
.navigationTitle("Favourites") .navigationTitle(String(localized: "Favourites", comment: "Favourites tab title"))
.toolbar { .toolbar {
if !favourites.isEmpty { if !favourites.isEmpty {
ToolbarItemGroup(placement: .topBarTrailing) { ToolbarItemGroup(placement: .topBarTrailing) {
@@ -164,7 +164,7 @@ struct FavouritesView: View {
showTrends = true showTrends = true
} label: { } label: {
Image(systemName: "chart.xyaxis.line") Image(systemName: "chart.xyaxis.line")
.accessibilityLabel("Trends") .accessibilityLabel(String(localized: "Trends", comment: "Trends button accessibility label"))
} }
EditButton() EditButton()
} }
+16
View File
@@ -182,3 +182,19 @@
"Check your internet connection" = "Check your internet connection"; "Check your internet connection" = "Check your internet connection";
"Tap to try again" = "Tap to try again"; "Tap to try again" = "Tap to try again";
"Check your internet connection. Tap to try again" = "Check your internet connection. Tap to try again"; "Check your internet connection. Tap to try again" = "Check your internet connection. Tap to try again";
/* Alerts and favourites localisation pass */
"Cheapest favourite" = "Cheapest favourite";
"Check now" = "Check now";
"Price-drop alerts" = "Price-drop alerts";
"Watches your cheapest saved favourite for the fuel you pick here. FuelBoard alerts when a different favourite becomes the cheapest, or when your current cheapest favourite drops by at least 1.0p. This is separate from the nearby-geofence alert, so you can monitor favourites without extra location noise." = "Watches your cheapest saved favourite for the fuel you pick here. FuelBoard alerts when a different favourite becomes the cheapest, or when your current cheapest favourite drops by at least 1.0p. This is separate from the nearby-geofence alert, so you can monitor favourites without extra location noise.";
"Cheapest station" = "Cheapest station";
"Proximity alerts" = "Proximity alerts";
"When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling %@ are watched — favourites get priority — and each station alerts at most once per hour. Follow search mirrors the Stations-tab distance, capped at 8 miles for reliable geofencing. Smaller radii ping more often but later; larger radii ping rarely in town but earlier from afar." = "When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Nearby stations selling %@ are watched — favourites get priority — and each station alerts at most once per hour. Follow search mirrors the Stations-tab distance, capped at 8 miles for reliable geofencing. Smaller radii ping more often but later; larger radii ping rarely in town but earlier from afar.";
"Set-and-forget: alerts behave exactly like the Stations-tab search distance (capped at 8 miles for reliable geofencing)." = "Set-and-forget: alerts behave exactly like the Stations-tab search distance (capped at 8 miles for reliable geofencing).";
"Town driving. Fires most often — you're rarely inside the circle for long, so pings are meaningful. Best for stop-start local trips." = "Town driving. Fires most often — you're rarely inside the circle for long, so pings are meaningful. Best for stop-start local trips.";
"City or suburb commute. Slightly earlier heads-up than the smallest option; still fires regularly around town." = "City or suburb commute. Slightly earlier heads-up than the smallest option; still fires regularly around town.";
"Mixed driving. Middle ground: heads-up a few minutes out, still works in most towns." = "Mixed driving. Middle ground: heads-up a few minutes out, still works in most towns.";
"A-roads and motorway exits. Wide early warning for longer trips — but in town it rarely fires, because you're usually already inside the circle." = "A-roads and motorway exits. Wide early warning for longer trips — but in town it rarely fires, because you're usually already inside the circle.";
"Long-distance journeys. Maximum reach: a ping when a cheap station is coming up from far out. In town this essentially never fires." = "Long-distance journeys. Maximum reach: a ping when a cheap station is coming up from far out. In town this essentially never fires.";
"%@ — %@" = "%@ — %@";