diff --git a/common/AsyncImageWithPlaceholder.swift b/common/AsyncImageWithPlaceholder.swift new file mode 100644 index 0000000..c81cea9 --- /dev/null +++ b/common/AsyncImageWithPlaceholder.swift @@ -0,0 +1,8 @@ +// +// AsyncImageWithPlaceholder.swift +// meterios +// +// Created by Maximilian on 23.04.25. +// + +import Foundation diff --git a/common/Localizable.xcstrings b/common/Localizable.xcstrings new file mode 100644 index 0000000..4cad4b4 --- /dev/null +++ b/common/Localizable.xcstrings @@ -0,0 +1,565 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + }, + "shouldTranslate" : false + }, + "?" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "?" + } + } + }, + "shouldTranslate" : false + }, + "%@" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + } + }, + "shouldTranslate" : false + }, + "ACCOUNT" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konto" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Account" + } + } + } + }, + "AMOUNT_FMT" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anzahl: %ix" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Amount: %ix" + } + } + } + }, + "AVAILABLE_DRINKS" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verfügbar" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Available" + } + } + } + }, + "AVAILABLE_FMT" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ ist verfügbar" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ is available" + } + } + } + }, + "BALANCE" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kontostand" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Balance" + } + } + } + }, + "BOOKING" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buchen…" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Booking..." + } + } + } + }, + "CAFFEINE" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koffein" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Caffeine" + } + } + } + }, + "CONNECT" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbinden" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connect" + } + } + } + }, + "CONNECTING_TO_FMT" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zu %@ verbinden" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connecting to %@" + } + } + } + }, + "CONNECTION" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbindung" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connection" + } + } + } + }, + "CONNECTION_ERROR" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbindungsfehler" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connection Error" + } + } + } + }, + "CONNECTION_SUCCESSFUL" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbindung erfolgreich" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connection successful" + } + } + } + }, + "CONTACTING_BACKEND" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbinden zu Mete…" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contacting Mete backend..." + } + } + } + }, + "DRINKS" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Getränke" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Drinks" + } + } + } + }, + "ERROR_IN_COMMUNICATION" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fehler in Kommunikation" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error in communication" + } + } + } + }, + "FAVOURITE" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorit" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favourite" + } + } + } + }, + "FAVOURITES" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favoriten" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favourites" + } + } + } + }, + "INACTIVE_FMT" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ ist als inaktiv markiert." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ is marked inactive." + } + } + } + }, + "LOGOUT" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ausloggen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Log out" + } + } + } + }, + "METE_SERVER" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mete Server" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mete Server" + } + } + } + }, + "METERIOS" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meterios" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meterios" + } + } + } + }, + "METERIOS_SLOGAN" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pick your poison." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pick your poison." + } + } + } + }, + "NO_DRINKS" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine Drinks auf Server" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Server has no drinks" + } + } + } + }, + "OPEN_PROFILE_IN_BROWSER" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil im Browser öffnen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open profile in browser" + } + } + } + }, + "OUT_OF_STOCK_DRINKS" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nicht im Haus" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Out of stock" + } + } + } + }, + "POISON_PICKED" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sieht gut aus!" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Poison picked!" + } + } + } + }, + "PRICE" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preis" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Price" + } + } + } + }, + "REFRESH" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualisieren" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Refresh" + } + } + } + }, + "RETRY" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erneut versuchen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retry" + } + } + } + }, + "SAVE_CONNECTION_SETTINGS" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbindungsdaten speichern" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Save connection settings" + } + } + } + }, + "TOTAL_SUM" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Summe" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total sum" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/common/types/User.swift b/common/types/User.swift index f815549..b2f5bc1 100644 --- a/common/types/User.swift +++ b/common/types/User.swift @@ -23,7 +23,7 @@ struct User: Decodable, Identifiable, Hashable { self.meteID = try container.decode(Int.self, forKey: .id) self.displayName = try container.decode(String.self, forKey: .name) self.displayImageURL = String( - format: "https://secure.gravatar.com/avatar/%@", + format: "https://secure.gravatar.com/avatar/%@?s=300", Insecure.MD5.hash( // e-mails may be empty as they are not required during the registration process in Mete // however, Gravatar requires them for the profile picture functionality. diff --git a/meterios-watch/views/ContentView.swift b/meterios-watch/views/ContentView.swift index a77a29d..ada199d 100644 --- a/meterios-watch/views/ContentView.swift +++ b/meterios-watch/views/ContentView.swift @@ -52,14 +52,14 @@ struct ContentView: View { NavigationView { List() { // Drinks - Section("Favourites") { + Section(String(localized: "FAVOURITES")) { ForEach(favouriteDrinks) { drink in NavigationLink(drink.name) { DrinkDetailView(wcMgr: wcMgr, drink: drink) } } } - Section("Drinks") { + Section(String(localized: "DRINKS")) { ForEach(availableDrinks) { drink in NavigationLink(drink.name) { DrinkDetailView(wcMgr: wcMgr, drink: drink) @@ -69,7 +69,7 @@ struct ContentView: View { // Controls Section { - Button("Refresh") { + Button(String(localized: "REFRESH")) { favouriteDrinks = [] availableDrinks = [] update() @@ -83,9 +83,9 @@ struct ContentView: View { } .onAppear(perform: update) // Alert to communicate an error in communication with the paired device - .alert("Connection Error", isPresented: $showUpdateError) { + .alert(String(localized: "CONNECTION_ERROR"), isPresented: $showUpdateError) { Text(updateError).tint(.red).font(.footnote) - Button("Retry", systemImage: "arrow.clockwise", action: update) + Button(String(localized: "RETRY"), systemImage: "arrow.clockwise", action: update) } } } diff --git a/meterios-watch/views/DrinkDetailView.swift b/meterios-watch/views/DrinkDetailView.swift index f4c34a4..8d618e1 100644 --- a/meterios-watch/views/DrinkDetailView.swift +++ b/meterios-watch/views/DrinkDetailView.swift @@ -29,12 +29,12 @@ struct DrinkDetailView: View { HStack { VStack { Text(String(format: "%img", drink.caffeine)).fontWeight(.bold) - Text("Caffeine").fontWeight(Font.Weight.thin) + Text(String(localized: "CAFFEINE")).fontWeight(Font.Weight.thin) } Divider() VStack { Text(String(format: "%.2f€", drink.GetPrice())).fontWeight(.bold) - Text("Price").fontWeight(Font.Weight.thin) + Text(String(localized: "PRICE")).fontWeight(Font.Weight.thin) } } @@ -43,7 +43,7 @@ struct DrinkDetailView: View { // Open the transaction sheet shouldShowTransactionSheet = true }) { - Text("Poison picked!") + Text(String(localized: "POISON_PICKED")) }.tint(.green) } diff --git a/meterios-watch/views/TransactionView.swift b/meterios-watch/views/TransactionView.swift index d53cbf8..5b363bf 100644 --- a/meterios-watch/views/TransactionView.swift +++ b/meterios-watch/views/TransactionView.swift @@ -11,7 +11,7 @@ struct TransactionView: View { private var wcMgr: WCManager private var drinkID: Int - @State var statusLine = "Booking..." + @State var statusLine = String(localized: "BOOKING") @Environment(\.dismiss) private var dismiss diff --git a/meterios.xcodeproj/project.pbxproj b/meterios.xcodeproj/project.pbxproj index 03619d9..ab2a9a2 100644 --- a/meterios.xcodeproj/project.pbxproj +++ b/meterios.xcodeproj/project.pbxproj @@ -50,6 +50,7 @@ A7FA4CCB2D9E9564005ACDBB /* Exceptions for "common" folder in "meterios-watch" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + Localizable.xcstrings, SalutManager.swift, types/Drink.swift, types/User.swift, @@ -194,6 +195,7 @@ knownRegions = ( en, Base, + de, ); mainGroup = A7F97A852D8DD542004EF4ED; minimizedProjectReferenceProxies = 1; @@ -312,6 +314,7 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; @@ -370,6 +373,7 @@ MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; VALIDATE_PRODUCT = YES; }; name = Release; @@ -398,7 +402,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -435,7 +439,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -465,7 +469,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios.watchkitapp; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; @@ -494,7 +498,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = space.chaosdorf.meterios.watchkitapp; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; diff --git a/meterios/views/DrinkDetailView.swift b/meterios/views/DrinkDetailView.swift index 96387c1..84a2cd6 100644 --- a/meterios/views/DrinkDetailView.swift +++ b/meterios/views/DrinkDetailView.swift @@ -63,12 +63,12 @@ struct DrinkDetail: View { HStack { VStack { Text(String(format: "%img", drink.caffeine)).fontWeight(.bold) - Text("Caffeine").fontWeight(Font.Weight.thin) + Text(String(localized: "CAFFEINE")).fontWeight(Font.Weight.thin) }.fixedSize() Divider() VStack { - Text(String(format: "%.2f€", drink.GetPrice())).fontWeight(.bold) - Text("Price").fontWeight(Font.Weight.thin) + Text(drink.GetPrice().formatted(.currency(code: "EUR"))).fontWeight(.bold) + Text(String(localized: "PRICE")).fontWeight(Font.Weight.thin) } } } @@ -79,7 +79,7 @@ struct DrinkDetail: View { // Favourites Section() { - Toggle("Favourite", isOn: $isFavourite).toggleStyle(SwitchToggleStyle(tint: .blue)) + Toggle(String(localized: "FAVOURITE"), isOn: $isFavourite).toggleStyle(SwitchToggleStyle(tint: .blue)) .onAppear(perform: { isFavourite = FavouritesManager.default.IsFavourite(drink.id) }) @@ -90,7 +90,7 @@ struct DrinkDetail: View { // Summary Section { - Stepper("Amount: **\(self.drinkAmount)x**", onIncrement: { + Stepper(String(format: String(localized: "AMOUNT_FMT"), self.drinkAmount), onIncrement: { if self.drinkAmount < 10 { self.drinkAmount += 1 } @@ -100,18 +100,18 @@ struct DrinkDetail: View { } }) HStack { - Text("Total sum:") + Text(String(localized: "TOTAL_SUM")) Spacer() - Text(String(format: "%.2f€", self.drink.GetPrice() * Float(self.drinkAmount))).fontWeight(.bold) + Text((self.drink.GetPrice() * Float(self.drinkAmount)).formatted(.currency(code: "EUR"))).fontWeight(.bold) } } // Warning for inactive bewerages if !drink.active { Section( - header: Text("\(drink.name) is marked inactive."), + header: Text(String(format: String(localized: "INACTIVE_FMT"), drink.name)), content: { - Toggle("\(drink.name) is available", isOn: $overrideDeactivatedState).toggleStyle(SwitchToggleStyle(tint: .green)) + Toggle(String(format: String(localized: "AVAILABLE_FMT"), drink.name), isOn: $overrideDeactivatedState).toggleStyle(SwitchToggleStyle(tint: .green)) }) } @@ -120,7 +120,7 @@ struct DrinkDetail: View { // Open the transaction sheet shouldShowTransactionSheet = true }) { - Text("Poison picked!").frame(maxWidth: .infinity) + Text(String(localized: "POISON_PICKED")).frame(maxWidth: .infinity) }.tint(.green).disabled(!drink.active && !overrideDeactivatedState) } diff --git a/meterios/views/MainView.swift b/meterios/views/MainView.swift index f02d1d9..81fd2eb 100644 --- a/meterios/views/MainView.swift +++ b/meterios/views/MainView.swift @@ -27,20 +27,57 @@ struct MainView: View { let drinks = try await BackendConnector().GetDrinks(baseAddr: meteHostAddr) favouriteDrinks = drinks.filter({ FavouritesManager.default.IsFavourite($0.id) && $0.active }) availableDrinks = drinks.filter({ !FavouritesManager.default.IsFavourite($0.id) && $0.active }) - outOfOrderDrinks = drinks.filter({ !FavouritesManager.default.IsFavourite($0.id) && !$0.active }) + outOfOrderDrinks = drinks.filter({ !$0.active }) } catch { let _ = Logger().error("Failed to update from \(meteHostAddr): \(error)") } } } + func drinkSection(title: String, drinks: Array) -> some View { + Section(header: Text(title)) { + ForEach(drinks) { drink in + NavigationLink(destination: { + DrinkDetail(drink: drink).onDisappear(perform: { + update() + }) + }, label: { + AsyncImage(url: URL(string: "https://\(meteHostAddr)/\(drink.logoURL)")) { phase in + if let image = phase.image { + // Image loaded + image + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 32, height: 32) + .clipShape(RoundedRectangle(cornerRadius: 8.0)) + } else if phase.error != nil { + // Error, display a dummy image (question mark) + let _ = Logger().error("Failed to load drink image for \(drink.name): \(phase.error)") + ZStack { + RoundedRectangle(cornerRadius: 8.0).foregroundStyle(.white).tint(.gray).frame(width: 32, height: 32) + Text("?").fontWeight(.bold) + } + } else { + // Loading Placeholder + ZStack { + RoundedRectangle(cornerRadius: 8.0).foregroundStyle(.white).tint(.gray).frame(width: 32, height: 32) + ProgressView() + } + } + }.fixedSize() + Text(drink.name) + }) + } + } + } + var body: some View { ScrollViewReader { scrollViewReader in NavigationView { VStack(alignment: .leading) { List () { // Account - Section(header: Text("Account").id("topAnchor")) { + Section(header: Text(String(localized: "ACCOUNT")).id("topAnchor")) { NavigationLink(destination: UserDetailView(user: user)) { Button(action: { dismiss() @@ -65,7 +102,7 @@ struct MainView: View { HStack { Text(user.displayName).fontWeight(.bold) Divider() - Text(String(format: "%.2f€", user.balance)).foregroundStyle(user.balance > 0 ? .green : .red) + Text(user.balance.formatted(.currency(code: "EUR"))).foregroundStyle(user.balance > 0 ? .green : .red) }.fixedSize() Text("\(meteHostAddr)").fontWeight(.thin) }.fixedSize() @@ -76,39 +113,29 @@ struct MainView: View { // Drinks list if !favouriteDrinks.isEmpty { - Section(header: Text("Favourites")) { - ForEach(favouriteDrinks) { drink in - NavigationLink(drink.name) { - DrinkDetail(drink: drink).onDisappear(perform: { - update() - scrollViewReader.scrollTo("topAnchor", anchor: .top) - }) - } - } - } + drinkSection(title: String(localized: "FAVOURITES"), drinks: favouriteDrinks) } if !availableDrinks.isEmpty { - Section(header: Text("Available")) { - ForEach(availableDrinks) { drink in - NavigationLink(drink.name) { - DrinkDetail(drink: drink).onDisappear(perform: { - update() - scrollViewReader.scrollTo("topAnchor", anchor: .top) - }) - } - } - } + drinkSection(title: String(localized: "AVAILABLE_DRINKS"), drinks: availableDrinks) } if !outOfOrderDrinks.isEmpty { - Section(header: Text("Out of stock")) { - ForEach(outOfOrderDrinks) { drink in - NavigationLink(drink.name) { - DrinkDetail(drink: drink) - } - } - } + drinkSection(title: String(localized: "OUT_OF_STOCK_DRINKS"), drinks: outOfOrderDrinks) } - }.navigationTitle("Meterios").refreshable(action: { + // Catch a backend without drinks + if favouriteDrinks.isEmpty && availableDrinks.isEmpty && outOfOrderDrinks.isEmpty { + Section(content: {}, footer: { + HStack(alignment: .center, content: { + Spacer() + VStack() { + Image(systemName: "bolt.horizontal.circle.fill").font(.title).foregroundStyle(.gray) + Spacer(minLength: 8) + Text(String(localized: "NO_DRINKS")).font(.title).foregroundStyle(.gray) + } + Spacer() + }) + }) + } + }.navigationTitle(String(localized: "METERIOS")).refreshable(action: { update() }) } diff --git a/meterios/views/SetupView.swift b/meterios/views/SetupView.swift index 110eb09..094fbd6 100644 --- a/meterios/views/SetupView.swift +++ b/meterios/views/SetupView.swift @@ -40,7 +40,7 @@ struct SetupView: View { saveButtonEnabled = false // Update status - connectionStatus = "Connecting to \(tmpAddress)" + connectionStatus = String(format: String(localized: "CONNECTING_TO_FMT"), tmpAddress) connectionStatusIcon = "network" connectionStatusColor = .blue connectionStatusProgressVisible = true @@ -55,7 +55,7 @@ struct SetupView: View { do { // Try out the connection tmpUsers = try await BackendConnector().GetUsers(baseAddr: tmpAddress) - connectionStatus = "Connection successful" + connectionStatus = String(localized: "CONNECTION_SUCCESSFUL") connectionStatusIcon = "checkmark.icloud.fill" connectionStatusColor = .green connectionStatusProgressVisible = false @@ -90,8 +90,8 @@ struct SetupView: View { .fixedSize() Spacer() VStack(alignment: .leading) { - Text("Meterios").font(.title).fontWeight(.heavy).foregroundStyle(.white) - Text("Pick your poison.").font(.title2).foregroundStyle(.white) + Text(String(localized: "METERIOS")).font(.title).fontWeight(.heavy).foregroundStyle(.white) + Text(String(localized: "METERIOS_SLOGAN")).font(.title2).foregroundStyle(.white) }.fixedSize() }.fixedSize() } @@ -109,11 +109,11 @@ struct SetupView: View { tryConnect() } - Button("Connect", action: { + Button(String(localized: "CONNECT"), action: { tryConnect() }) }, header: { - Text("Connection") + Text(String(localized: "CONNECTION")) }, footer: { // Status bar if connectionStatusVisible { @@ -133,12 +133,11 @@ struct SetupView: View { // User selection if userListVisible { - Picker("Account", selection: $selectedUserID, content: { + Picker(String(localized: "ACCOUNT"), selection: $selectedUserID, content: { ForEach(tmpUsers, id: \.self.meteID) { (user: User) in Text(user.displayName).tag(user.displayName) } }) - .pickerStyle(.inline) .onChange(of: selectedUserID, { saveButtonEnabled = true }) @@ -151,7 +150,7 @@ struct SetupView: View { meteUserID = selectedUserID dismiss() }) { - Text("Save connection settings").frame(maxWidth: .infinity) + Text(String(localized: "SAVE_CONNECTION_SETTINGS")).frame(maxWidth: .infinity) }.tint(.green).disabled(!saveButtonEnabled) } } diff --git a/meterios/views/TransactionView.swift b/meterios/views/TransactionView.swift index b0b3861..d3642d5 100644 --- a/meterios/views/TransactionView.swift +++ b/meterios/views/TransactionView.swift @@ -14,7 +14,7 @@ struct TransactionView: View { var drinkID: Int var drinkAmount: Int - @State var statusLine = "Contacting Mete backend..." + @State var statusLine = String(localized: "CONTACTING_BACKEND") @Environment(\.dismiss) private var dismiss @@ -50,7 +50,7 @@ struct TransactionView: View { maybeDismiss() } catch { // Error occurred - statusLine = "Error in communication" + statusLine = String(localized: "ERROR_IN_COMMUNICATION") print(error) } } diff --git a/meterios/views/UserDetailView.swift b/meterios/views/UserDetailView.swift index 6f74c33..d335a6a 100644 --- a/meterios/views/UserDetailView.swift +++ b/meterios/views/UserDetailView.swift @@ -40,14 +40,14 @@ struct UserDetailView: View { // Balance and Mete backend address HStack { VStack { - Text(String(format: "%.2f€", user.balance)).fontWeight(.bold) + Text(user.balance.formatted(.currency(code: "EUR"))).fontWeight(.bold) .foregroundStyle(user.balance > 0 ? .green : .red) - Text("Balance").fontWeight(Font.Weight.thin) + Text(String(localized: "BALANCE")).fontWeight(Font.Weight.thin) }.fixedSize() Divider() VStack { Text(String(meteHostAddr)) - Text("Mete Server").fontWeight(Font.Weight.thin) + Text(String(localized: "METE_SERVER")).fontWeight(Font.Weight.thin) } } }.fixedSize() @@ -61,7 +61,7 @@ struct UserDetailView: View { Button(action: { UIApplication.shared.open(URL(string: "https://\(meteHostAddr)/users/\(meteUserID)")!) }) { - Text("Open profile in browser").frame(maxWidth: .infinity) + Text(String(localized: "OPEN_PROFILE_IN_BROWSER")).frame(maxWidth: .infinity) } } @@ -73,7 +73,7 @@ struct UserDetailView: View { meteUserID = -1 dismiss() }) { - Text("Log out").frame(maxWidth: .infinity) + Text(String(localized: "LOGOUT")).frame(maxWidth: .infinity) }.tint(.red) } }