2
0
Эх сурвалжийг харах

Fix improper string formatting

MMP0 3 жил өмнө
parent
commit
67fb0a09b7
32 өөрчлөгдсөн 112 нэмэгдсэн , 59 устгасан
  1. 1 1
      Configuration/Legacy/UTMLegacyAppleConfiguration.swift
  2. 1 1
      Configuration/QEMUConstant.swift
  3. 2 2
      Configuration/UTMConfiguration.swift
  4. 1 1
      Managers/UTMAppleVirtualMachine.swift
  5. 1 1
      Managers/UTMDrive.m
  6. 2 2
      Managers/UTMPendingVirtualMachine.swift
  7. 4 4
      Managers/UTMQemuVirtualMachine.m
  8. 4 4
      Platform/Shared/NumberTextField.swift
  9. 2 2
      Platform/Shared/RAMSlider.swift
  10. 1 1
      Platform/Shared/UTMPendingVMView.swift
  11. 1 1
      Platform/Shared/UTMPlaceholderVMView.swift
  12. 1 1
      Platform/Shared/VMConfigSystemView.swift
  13. 2 2
      Platform/Shared/VMDetailsView.swift
  14. 15 7
      Platform/Shared/VMWizardOSLinuxView.swift
  15. 1 1
      Platform/Shared/VMWizardOSOtherView.swift
  16. 1 1
      Platform/Shared/VMWizardSharingView.swift
  17. 5 3
      Platform/Shared/VMWizardSummaryView.swift
  18. 3 3
      Platform/UTMData.swift
  19. 22 0
      Platform/en.lproj/Localizable.stringsdict
  20. 3 1
      Platform/iOS/VMConfigNetworkPortForwardView.swift
  21. 4 4
      Platform/iOS/VMDrivesSettingsView.swift
  22. 1 1
      Platform/macOS/Display/VMDisplayAppleTerminalWindowController.swift
  23. 3 1
      Platform/macOS/Display/VMDisplayAppleWindowController.swift
  24. 1 1
      Platform/macOS/Display/VMDisplayQemuDisplayController.swift
  25. 3 3
      Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift
  26. 1 1
      Platform/macOS/Display/VMDisplayQemuTerminalWindowController.swift
  27. 1 1
      Platform/macOS/KeyCodeMap.swift
  28. 3 3
      Platform/macOS/SavePanel.swift
  29. 1 2
      Platform/macOS/VMAppleRemovableDrivesView.swift
  30. 3 1
      Platform/macOS/VMConfigNetworkPortForwardView.swift
  31. 2 2
      Platform/macOS/VMDrivesSettingsView.swift
  32. 16 0
      UTM.xcodeproj/project.pbxproj

+ 1 - 1
Configuration/Legacy/UTMLegacyAppleConfiguration.swift

@@ -170,7 +170,7 @@ final class UTMLegacyAppleConfiguration: Codable {
         consoleBackgroundColor = try values.decodeIfPresent(String.self, forKey: .consoleBackgroundColor)
         consoleFont = try values.decodeIfPresent(String.self, forKey: .consoleFont)
         let fontSize = try values.decodeIfPresent(Int.self, forKey: .consoleFontSize)
-        consoleFontSize = NSNumber(value: fontSize ?? 12)
+        consoleFontSize = (fontSize ?? 12) as NSNumber
         consoleCursorBlink = try values.decode(Bool.self, forKey: .consoleCursorBlink)
         consoleResizeCommand = try values.decodeIfPresent(String.self, forKey: .consoleResizeCommand)
     }

+ 1 - 1
Configuration/QEMUConstant.swift

@@ -264,7 +264,7 @@ struct QEMUTerminalFont: QEMUConstant {
             } else {
                 description = NSLocalizedString("Regular", comment: "UTMQemuConstants")
             }
-            return "\(font.familyName) (\(description)"
+            return String.localizedStringWithFormat(NSLocalizedString("%@ (%@)", comment: "QEMUConstant"), font.familyName, description)
         }
     }()
     #endif

+ 2 - 2
Configuration/UTMConfiguration.swift

@@ -61,9 +61,9 @@ extension UTMConfigurationError: LocalizedError {
         switch self {
         case .versionTooLow: return NSLocalizedString("This configuration is too old and is not supported.", comment: "UTMConfiguration")
         case .versionTooHigh: return NSLocalizedString("This configuration is saved with a newer version of UTM and is not compatible with this version.", comment: "UTMConfiguration")
-        case .invalidConfigurationValue(let value): return NSLocalizedString("An invalid value of '\(value)' is used in the configuration file.", comment: "UTMConfiguration")
+        case .invalidConfigurationValue(let value): return String.localizedStringWithFormat(NSLocalizedString("An invalid value of '%@' is used in the configuration file.", comment: "UTMConfiguration"), value)
         case .invalidBackend: return NSLocalizedString("The backend for this configuration is not supported.", comment: "UTMConfiguration")
-        case .driveAlreadyExists(let url): return NSLocalizedString("The drive '\(url.lastPathComponent)' already exists and cannot be created.", comment: "UTMConfiguration")
+        case .driveAlreadyExists(let url): return String.localizedStringWithFormat(NSLocalizedString("The drive '%@' already exists and cannot be created.", comment: "UTMConfiguration"), url.lastPathComponent)
         default: return NSLocalizedString("An internal error has occurred.", comment: "UTMConfiguration")
         }
     }

+ 1 - 1
Managers/UTMAppleVirtualMachine.swift

@@ -448,7 +448,7 @@ extension UTMAppleVirtualMachineError: LocalizedError {
     var errorDescription: String? {
         switch self {
         case .cannotAccessResource(let url):
-            return NSLocalizedString("Cannot access resource: \(url.path)", comment: "UTMAppleVirtualMachine")
+            return String.localizedStringWithFormat(NSLocalizedString("Cannot access resource: %@", comment: "UTMAppleVirtualMachine"), url.path)
         }
     }
 }

+ 1 - 1
Managers/UTMDrive.m

@@ -25,7 +25,7 @@
     if (!filename) {
         filename = NSLocalizedString(@"none", @"UTMDrive");
     }
-    return [NSString stringWithFormat:@"%@ (%@): %@", imageTypeStr, self.interface, filename];
+    return [NSString localizedStringWithFormat:@"%@ (%@): %@", imageTypeStr, self.interface, filename];
 }
 
 @end

+ 2 - 2
Managers/UTMPendingVirtualMachine.swift

@@ -84,7 +84,7 @@ import Foundation
             return
         }
         let localizedFormatString = NSLocalizedString("%@ remaining", comment: "Format string for remaining time until a download finishes")
-        self.estimatedTimeRemaining = String(format: localizedFormatString, etaString)
+        self.estimatedTimeRemaining = String.localizedStringWithFormat(localizedFormatString, etaString)
     }
     
     private func updateDownloadStats(for newBytesWritten: Int64, currentTotal totalBytesWritten: Int64, estimatedTotal totalBytesExpectedToWrite: Int64) {
@@ -100,7 +100,7 @@ import Foundation
         let bytesString = ByteCountFormatter.string(fromByteCount: bytesPerSecond, countStyle: .file)
         let speedFormat = NSLocalizedString("%@ / s",
                                             comment: "Format string for the 'per second' part of a download speed.")
-        estimatedDownloadSpeed = String(format: speedFormat, bytesString)
+        estimatedDownloadSpeed = String.localizedStringWithFormat(speedFormat, bytesString)
         /// sizes
         downloadedSize = ByteCountFormatter.string(fromByteCount: totalBytesWritten, countStyle: .file)
         estimatedDownloadSize = ByteCountFormatter.string(fromByteCount: totalBytesExpectedToWrite, countStyle: .file)

+ 4 - 4
Managers/UTMQemuVirtualMachine.m

@@ -205,7 +205,7 @@ NSString *const kSuspendSnapshotName = @"suspend";
         }
         if (!success) {
             if (!msg) {
-                msg = [NSString stringWithFormat:NSLocalizedString(@"QEMU exited from an error: %@", @"UTMQemuVirtualMachine"), self.lastErrorLine];
+                msg = [NSString localizedStringWithFormat:NSLocalizedString(@"QEMU exited from an error: %@", @"UTMQemuVirtualMachine"), self.lastErrorLine];
             }
             qemuStartError = [_self errorWithMessage:msg];
             dispatch_semaphore_signal(spiceConnectOrErrorEvent);
@@ -264,12 +264,12 @@ NSString *const kSuspendSnapshotName = @"suspend";
     assert(self.qemu.isConnected);
     // set up SPICE sharing and removable drives
     if (![self startSharedDirectoryWithError:&err]) {
-        errMsg = [NSString stringWithFormat:NSLocalizedString(@"Error trying to start shared directory: %@", @"UTMVirtualMachine"), err.localizedDescription];
+        errMsg = [NSString localizedStringWithFormat:NSLocalizedString(@"Error trying to start shared directory: %@", @"UTMVirtualMachine"), err.localizedDescription];
         completion([self errorWithMessage:errMsg]);
         return;
     }
     if (![self restoreRemovableDrivesFromBookmarksWithError:&err]) {
-        errMsg = [NSString stringWithFormat:NSLocalizedString(@"Error trying to restore removable drives: %@", @"UTMVirtualMachine"), err.localizedDescription];
+        errMsg = [NSString localizedStringWithFormat:NSLocalizedString(@"Error trying to restore removable drives: %@", @"UTMVirtualMachine"), err.localizedDescription];
         completion([self errorWithMessage:errMsg]);
         return;
     }
@@ -460,7 +460,7 @@ NSString *const kSuspendSnapshotName = @"suspend";
         }
         if (saveError) {
             // replace error with detailed message
-            NSString *newMsg = [NSString stringWithFormat:NSLocalizedString(@"Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@", @"UTMQemuVirtualMachine"), saveError.localizedDescription];
+            NSString *newMsg = [NSString localizedStringWithFormat:NSLocalizedString(@"Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@", @"UTMQemuVirtualMachine"), saveError.localizedDescription];
             saveError = [self errorWithMessage:newMsg];
         }
         dispatch_semaphore_signal(saveTriggeredEvent);

+ 4 - 4
Platform/Shared/NumberTextField.swift

@@ -44,7 +44,7 @@ struct NumberTextFieldOld: View {
                 return number.intValue == 0 ? "" : string
             }, set: {
                 // make sure we never set nil
-                self.number = self.formatter.number(from: $0) ?? NSNumber(value: 0)
+                self.number = self.formatter.number(from: $0) ?? 0
             }), onEditingChanged: onEditingChanged)
                 .keyboardType(.numberPad)
                 .multilineTextAlignment(.trailing)
@@ -110,7 +110,7 @@ struct NumberTextField: View {
     
     init(_ titleKey: LocalizedStringKey, number: Binding<Int>, prompt: LocalizedStringKey = "0", onEditingChanged: @escaping (Bool) -> Void = { _ in }) {
         let nsnumber = Binding<NSNumber?> {
-            return NSNumber(value: number.wrappedValue)
+            return number.wrappedValue as NSNumber
         } set: { newValue in
             number.wrappedValue = newValue?.intValue ?? 0
         }
@@ -147,13 +147,13 @@ extension NSNumber {
             let formatter = NumberFormatter()
             formatter.usesGroupingSeparator = false
             formatter.usesSignificantDigits = false
-            return formatter.number(from: value) ?? NSNumber(value: 0)
+            return formatter.number(from: value) ?? 0
         }
     }
 }
 
 struct NumberTextField_Previews: PreviewProvider {
     static var previews: some View {
-        NumberTextField("Test", number: .constant(NSNumber(value: 123)))
+        NumberTextField("Test", number: .constant(123 as NSNumber))
     }
 }

+ 2 - 2
Platform/Shared/RAMSlider.swift

@@ -42,7 +42,7 @@ struct RAMSlider: View {
     init<T: FixedWidthInteger>(systemMemory: Binding<T>, onValidate: @escaping (Bool) -> Void = { _ in }) {
         validateMemorySize = onValidate
         _systemMemory = Binding<NSNumber?>(get: {
-            NSNumber(value: UInt64(systemMemory.wrappedValue))
+            UInt64(systemMemory.wrappedValue) as NSNumber
         }, set: { newValue in
             systemMemory.wrappedValue = T(newValue?.uint64Value ?? 0)
         })
@@ -82,7 +82,7 @@ struct RAMSlider: View {
         guard i >= 0 && i < validMemoryValues.count else {
             return 0
         }
-        return NSNumber(value: validMemoryValues[i])
+        return validMemoryValues[i] as NSNumber
     }
 }
 

+ 1 - 1
Platform/Shared/UTMPendingVMView.swift

@@ -37,7 +37,7 @@ fileprivate struct PendingVMDetailsView: View {
             if let currentSize = vm.downloadedSize, let estimatedSize = vm.estimatedDownloadSize {
                 let estimatedSpeed = vm.estimatedDownloadSpeed ?? NSLocalizedString("Extracting…", comment: "Word for decompressing a compressed folder")
                 let formatString = NSLocalizedString("%1$@ of %2$@ (%3$@)", comment: "Format string for download progress and speed, e. g. 5 MB of 6 GB (200 kbit/s)")
-                return String(format: formatString, currentSize, estimatedSize, estimatedSpeed)
+                return String.localizedStringWithFormat(formatString, currentSize, estimatedSize, estimatedSpeed)
             } else {
                 return NSLocalizedString("Preparing…", comment: "A download process is about to begin.")
             }

+ 1 - 1
Platform/Shared/UTMPlaceholderVMView.swift

@@ -90,7 +90,7 @@ struct MinimalProgressView: View {
         let formatter = NumberFormatter()
         formatter.numberStyle = .percent
         formatter.allowsFloats = false
-        let label = formatter.string(from: NSNumber(value: fractionCompleted)) ?? ""
+        let label = formatter.string(from: fractionCompleted as NSNumber) ?? ""
         return label
     }
     

+ 1 - 1
Platform/Shared/VMConfigSystemView.swift

@@ -145,7 +145,7 @@ private enum WarningMessage: Identifiable {
         switch self {
         case .overallocatedRam(let totalMib, let estimatedMib):
             let format = NSLocalizedString("Your device has %llu MB of memory and the estimated usage is %llu MB.", comment: "VMConfigSystemView")
-            return String(format: format, totalMib, estimatedMib)
+            return String.localizedStringWithFormat(format, totalMib, estimatedMib)
         case .resetSystem:
             return NSLocalizedString("Any unsaved changes will be lost.", comment: "VMConfigSystemView")
         }

+ 2 - 2
Platform/Shared/VMDetailsView.swift

@@ -290,11 +290,11 @@ private struct OptionalSelectableText: View {
     
     var body: some View {
         if #available(iOS 15, macOS 12, *) {
-            Text(content ?? NSLocalizedString("Inactive", comment: "VMDetailsView"))
+            content.map { Text($0) } ?? Text("Inactive", comment: "VMDetailsView")
                 .foregroundColor(.secondary)
                 .textSelection(.enabled)
         } else {
-            Text(content ?? NSLocalizedString("Inactive", comment: "VMDetailsView"))
+            content.map { Text($0) } ?? Text("Inactive", comment: "VMDetailsView")
                 .foregroundColor(.secondary)
         }
     }

+ 15 - 7
Platform/Shared/VMWizardOSLinuxView.swift

@@ -84,7 +84,7 @@ struct VMWizardOSLinuxView: View {
             if wizardState.useLinuxKernel {
                 
                 Section {
-                    Text(wizardState.linuxKernelURL?.lastPathComponent ?? "Empty")
+                    (wizardState.linuxKernelURL?.lastPathComponent.map { Text($0) } ?? Text("Empty"))
                         .font(.caption)
                     Button {
                         selectImage = .kernel
@@ -94,11 +94,15 @@ struct VMWizardOSLinuxView: View {
                     }
                     .padding(.leading, 1)
                 } header: {
-                    Text("\(wizardState.useAppleVirtualization ? "Uncompressed " : "")Linux kernel (required):")
+                    if wizardState.useAppleVirtualization {
+                        Text("Uncompressed \(Text("Linux kernel (required):"))")
+                    } else {
+                        Text("Linux kernel (required):")
+                    }
                 }
                 
                 Section {
-                    Text(wizardState.linuxInitialRamdiskURL?.lastPathComponent ?? "Empty")
+                    (wizardState.linuxInitialRamdiskURL?.lastPathComponent.map { Text($0) } ?? Text("Empty"))
                         .font(.caption)
 #if os(macOS)
                     HStack {
@@ -135,11 +139,15 @@ struct VMWizardOSLinuxView: View {
 #endif
                     
                 } header: {
-                    Text("\(wizardState.useAppleVirtualization ? "Uncompressed " : "")Linux initial ramdisk (optional):")
+                    if wizardState.useAppleVirtualization {
+                        Text("Uncompressed \(Text("Linux initial ramdisk (optional):"))")
+                    } else {
+                        Text("Linux initial ramdisk (optional):")
+                    }
                 }
                 
                 Section {
-                    Text(wizardState.linuxRootImageURL?.lastPathComponent ?? "Empty")
+                    (wizardState.linuxRootImageURL?.lastPathComponent.map { Text($0) } ?? Text("Empty"))
                         .font(.caption)
 #if os(macOS)
                     HStack {
@@ -174,7 +182,7 @@ struct VMWizardOSLinuxView: View {
                 }
                 
                 Section {
-                    Text(wizardState.bootImageURL?.lastPathComponent ?? "Empty")
+                    (wizardState.bootImageURL?.lastPathComponent.map { Text($0) } ?? Text("Empty"))
                         .font(.caption)
 #if os(macOS)
                     HStack {
@@ -226,7 +234,7 @@ struct VMWizardOSLinuxView: View {
             } else {
                 Section {
                     Text("Boot ISO Image:")
-                    Text(wizardState.bootImageURL?.lastPathComponent ?? "Empty")
+                    (wizardState.bootImageURL?.lastPathComponent.map { Text($0) } ?? Text("Empty"))
                         .font(.caption)
                     Button {
                         selectImage = .bootImage

+ 1 - 1
Platform/Shared/VMWizardOSOtherView.swift

@@ -29,7 +29,7 @@ struct VMWizardOSOtherView: View {
             if !wizardState.isSkipBootImage {
                 Section {
                     Text("Boot ISO Image:")
-                    Text(wizardState.bootImageURL?.lastPathComponent ?? "Empty")
+                    (wizardState.bootImageURL?.lastPathComponent.map { Text($0) } ?? Text("Empty"))
                         .font(.caption)
                     Button {
                         isFileImporterPresented.toggle()

+ 1 - 1
Platform/Shared/VMWizardSharingView.swift

@@ -30,7 +30,7 @@ struct VMWizardSharingView: View {
                 HStack {
                     Text("Directory")
                     Spacer()
-                    Text(wizardState.sharingDirectoryURL?.lastPathComponent ?? "Empty")
+                    (wizardState.sharingDirectoryURL?.lastPathComponent.map { Text($0) } ?? Text("Empty"))
                         .font(.caption)
                 }
                 if !wizardState.useAppleVirtualization {

+ 5 - 3
Platform/Shared/VMWizardSummaryView.swift

@@ -34,9 +34,11 @@ struct VMWizardSummaryView: View {
     
     var coreDescription: String {
         let cores = wizardState.systemCpuCount
-        let def = NSLocalizedString("Default", comment: "VMWizardSummaryView")
-        let suffix = cores == 1 ? NSLocalizedString("Core", comment: "VMWizardSummaryView") : NSLocalizedString("Cores", comment: "VMWizardSummaryView")
-        return "\(cores == 0 ? def : String(cores)) \(suffix)"
+        if cores == 0 {
+            return NSLocalizedString("Default Cores", comment: "VMWizardSummaryView")
+        } else {
+            return String.localizedStringWithFormat(NSLocalizedString("%lld Cores", comment: "VMWizardSummaryView"), cores)
+        }
     }
     
     var body: some View {

+ 3 - 3
Platform/UTMData.swift

@@ -790,7 +790,7 @@ class UTMData: ObservableObject {
         if event.wait(timeout: .now() + 10) == .timedOut {
             throw NSLocalizedString("Cannot find AltServer for JIT enable. You cannot run VMs until JIT is enabled.", comment: "UTMData")
         } else if let error = connectError {
-            throw NSLocalizedString("AltJIT error: \(error.localizedDescription)", comment: "UTMData")
+            throw String.localizedStringWithFormat(NSLocalizedString("AltJIT error: %@", comment: "UTMData"), error.localizedDescription)
         }
     }
 #endif
@@ -814,7 +814,7 @@ class UTMData: ObservableObject {
                 let (data, _) = try await URLSession.shared.data(for: request)
                 let attachResponse = try JSONDecoder().decode(AttachResponse.self, from: data)
                 if !attachResponse.success {
-                    attachError = String(format: NSLocalizedString("Failed to attach to JitStreamer:\n%@", comment: "ContentView"), attachResponse.message)
+                    attachError = String.localizedStringWithFormat(NSLocalizedString("Failed to attach to JitStreamer:\n%@", comment: "ContentView"), attachResponse.message)
                 }
             } catch is DecodingError {
                 throw NSLocalizedString("Failed to decode JitStreamer response.", comment: "ContentView")
@@ -825,7 +825,7 @@ class UTMData: ObservableObject {
                 throw attachError
             }
         } else {
-            throw String(format: NSLocalizedString("Invalid JitStreamer attach URL:\n%@", comment: "ContentView"), urlString)
+            throw String.localizedStringWithFormat(NSLocalizedString("Invalid JitStreamer attach URL:\n%@", comment: "ContentView"), urlString)
         }
     }
 

+ 22 - 0
Platform/en.lproj/Localizable.stringsdict

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>%lld Cores</key>
+	<dict>
+		<key>NSStringLocalizedFormatKey</key>
+		<string>%#@Cores@</string>
+		<key>Cores</key>
+		<dict>
+			<key>NSStringFormatSpecTypeKey</key>
+			<string>NSStringPluralRuleType</string>
+			<key>NSStringFormatValueTypeKey</key>
+			<string>lld</string>
+			<key>one</key>
+			<string>%lld Core</string>
+			<key>other</key>
+			<string>%lld Cores</string>
+		</dict>
+	</dict>
+</dict>
+</plist>

+ 3 - 1
Platform/iOS/VMConfigNetworkPortForwardView.swift

@@ -29,7 +29,9 @@ struct VMConfigNetworkPortForwardView: View {
                                                      onDelete: { config.portForward.removeAll(where: { $0 == forward }) }),
                         label: {
                             VStack(alignment: .leading) {
-                                Text(verbatim: "\(forward.guestAddress ?? ""):\(forward.guestPort) ➡️ \(forward.hostAddress ?? ""):\(forward.hostPort)")
+                                let guest = "\(forward.guestAddress ?? ""):\(forward.guestPort)"
+                                let host = "\(forward.hostAddress ?? ""):\(forward.hostPort)"
+                                Text("\(guest) ➡️ \(host)")
                                 Text(forward.protocol.prettyValue).font(.subheadline)
                             }
                         })

+ 4 - 4
Platform/iOS/VMDrivesSettingsView.swift

@@ -28,7 +28,7 @@ struct VMDrivesSettingsView: View {
         ForEach($config.drives) { $drive in
             NavigationLink(
                 destination: VMConfigDriveDetailsView(config: $drive, onDelete: nil), label: {
-                    Label(label(for: drive), systemImage: "externaldrive")
+                    Label(title: { labelTitle(for: drive) }, icon: { Image(systemName: "externaldrive") })
                 })
         }.onDelete { offsets in
             attemptDelete = offsets
@@ -44,11 +44,11 @@ struct VMDrivesSettingsView: View {
         }
     }
     
-    private func label(for drive: UTMQemuConfigurationDrive) -> String {
+    private func labelTitle(for drive: UTMQemuConfigurationDrive) -> Text {
         if drive.interface == .none && drive.imageName == QEMUPackageFileName.efiVariables.rawValue {
-            return NSLocalizedString("EFI Variables", comment: "VMDrivesSettingsView")
+            return Text("EFI Variables", comment: "VMDrivesSettingsView")
         } else {
-            return NSLocalizedString("\(drive.interface.prettyValue) Drive", comment: "VMDrivesSettingsView")
+            return Text("\(drive.interface.prettyValue) Drive", comment: "VMDrivesSettingsView")
         }
     }
     

+ 1 - 1
Platform/macOS/Display/VMDisplayAppleTerminalWindowController.swift

@@ -32,7 +32,7 @@ class VMDisplayAppleTerminalWindowController: VMDisplayAppleWindowController, VM
     
     override var defaultTitle: String {
         if isSecondary {
-            return NSLocalizedString("\(super.defaultTitle) (Terminal \(index+1))", comment: "VMDisplayAppleTerminalWindowController")
+            return String.localizedStringWithFormat(NSLocalizedString("%@ (Terminal %lld)", comment: "VMDisplayAppleTerminalWindowController"), super.defaultTitle, index + 1)
         } else {
             return super.defaultTitle
         }

+ 3 - 1
Platform/macOS/Display/VMDisplayAppleWindowController.swift

@@ -260,7 +260,9 @@ extension VMDisplayAppleWindowController {
             if progress >= 1 {
                 self.window!.subtitle = ""
             } else {
-                self.window!.subtitle = NSLocalizedString("Installation: \(Int(progress * 100))%", comment: "VMDisplayAppleWindowController")
+                let installationFormat = NSLocalizedString("Installation: %@", comment: "VMDisplayAppleWindowController")
+                let percentString = NumberFormatter.localizedString(from: progress as NSNumber, number: .percent)
+                self.window!.subtitle = String.localizedStringWithFormat(installationFormat, percentString)
             }
         }
     }

+ 1 - 1
Platform/macOS/Display/VMDisplayQemuDisplayController.swift

@@ -351,7 +351,7 @@ extension VMDisplayQemuWindowController: CSUSBManagerDelegate {
         let alert = NSAlert()
         alert.alertStyle = .informational
         alert.messageText = NSLocalizedString("USB Device", comment: "VMQemuDisplayMetalWindowController")
-        alert.informativeText = NSLocalizedString("Would you like to connect '\(usbDevice.name ?? usbDevice.description)' to this virtual machine?", comment: "VMQemuDisplayMetalWindowController")
+        alert.informativeText = String.localizedStringWithFormat(NSLocalizedString("Would you like to connect '%@' to this virtual machine?", comment: "VMQemuDisplayMetalWindowController"), usbDevice.name ?? usbDevice.description)
         alert.showsSuppressionButton = true
         alert.addButton(withTitle: NSLocalizedString("Confirm", comment: "VMQemuDisplayMetalWindowController"))
         alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "VMQemuDisplayMetalWindowController"))

+ 3 - 3
Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift

@@ -40,7 +40,7 @@ class VMQemuDisplayMetalWindowController: VMDisplayQemuWindowController {
     
     override var defaultTitle: String {
         if isSecondary {
-            return NSLocalizedString("\(vmQemuConfig.information.name) (Display \(id+1))", comment: "VMDisplayMetalWindowController")
+            return String.localizedStringWithFormat(NSLocalizedString("%@ (Display %lld)", comment: "VMDisplayMetalWindowController"), vmQemuConfig.information.name, id + 1)
         } else {
             return super.defaultTitle
         }
@@ -360,14 +360,14 @@ extension VMQemuDisplayMetalWindowController: VMMetalViewInputDelegate {
         let action = { () -> Void in
             self.qemuVM.requestInputTablet(false)
             self.metalView?.captureMouse()
-            self.window?.subtitle = NSLocalizedString("Press \(self.shouldUseCmdOptForCapture ? "⌘+⌥" : "⌃+⌥") to release cursor", comment: "VMQemuDisplayMetalWindowController")
+            self.window?.subtitle = String.localizedStringWithFormat(NSLocalizedString("Press %@ to release cursor", comment: "VMQemuDisplayMetalWindowController"), self.shouldUseCmdOptForCapture ? "⌘+⌥" : "⌃+⌥")
             self.window?.makeFirstResponder(self.metalView)
             self.syncCapsLock()
         }
         if isCursorCaptureAlertShown {
             let alert = NSAlert()
             alert.messageText = NSLocalizedString("Captured mouse", comment: "VMQemuDisplayMetalWindowController")
-            alert.informativeText = NSLocalizedString("To release the mouse cursor, press \(self.shouldUseCmdOptForCapture ? "⌘+⌥ (Cmd+Opt)" : "⌃+⌥ (Ctrl+Opt)") at the same time.", comment: "VMQemuDisplayMetalWindowController")
+            alert.informativeText = String.localizedStringWithFormat(NSLocalizedString("To release the mouse cursor, press %@ at the same time.", comment: "VMQemuDisplayMetalWindowController"), self.shouldUseCmdOptForCapture ? "⌘+⌥ (Cmd+Opt)" : "⌃+⌥ (Ctrl+Opt)")
             alert.showsSuppressionButton = true
             alert.beginSheetModal(for: window!) { _ in
                 if alert.suppressionButton?.state ?? .off == .on {

+ 1 - 1
Platform/macOS/Display/VMDisplayQemuTerminalWindowController.swift

@@ -26,7 +26,7 @@ class VMDisplayQemuTerminalWindowController: VMDisplayQemuWindowController, VMDi
     
     override var defaultTitle: String {
         if isSecondary {
-            return NSLocalizedString("\(vmQemuConfig.information.name) (Terminal \(id+1))", comment: "VMDisplayQemuTerminalWindowController")
+            return String.localizedStringWithFormat(NSLocalizedString("%@ (Terminal %lld)", comment: "VMDisplayQemuTerminalWindowController"), vmQemuConfig.information.name, id + 1)
         } else {
             return super.defaultTitle
         }

+ 1 - 1
Platform/macOS/KeyCodeMap.swift

@@ -74,7 +74,7 @@ class KeyCodeMap {
                         let modKeyIsUsed = ((modFlagDict[key]! & modifiers) != 0)
                         subDict[key] = NSNumber(booleanLiteral: modKeyIsUsed).intValue
                     }
-                    subDict["virtKeyCode"] = NSNumber(value: keyCode).intValue
+                    subDict["virtKeyCode"] = (keyCode as NSNumber).intValue
                     
                     // manipulate the NSEvent to get character produce by virtual key code and modifiers
                     var character: String

+ 3 - 3
Platform/macOS/SavePanel.swift

@@ -44,15 +44,15 @@ struct SavePanel: NSViewRepresentable {
             
             switch shareItem {
             case .debugLog:
-                savePanel.title = "Select where to save debug log:"
+                savePanel.title = NSLocalizedString("Select where to save debug log:", comment: "SavePanel")
                 savePanel.nameFieldStringValue = "debug"
                 savePanel.allowedContentTypes = [.appleLog]
             case .utmCopy(let vm), .utmMove(let vm):
-                savePanel.title = "Select where to save UTM Virtual Machine:"
+                savePanel.title = NSLocalizedString("Select where to save UTM Virtual Machine:", comment: "SavePanel")
                 savePanel.nameFieldStringValue = vm.path.lastPathComponent
                 savePanel.allowedContentTypes = [.UTM]
             case .qemuCommand:
-                savePanel.title = "Select where to export QEMU command:"
+                savePanel.title = NSLocalizedString("Select where to export QEMU command:", comment: "SavePanel")
                 savePanel.nameFieldStringValue = "command"
                 savePanel.allowedContentTypes = [.plainText]
             }

+ 1 - 2
Platform/macOS/VMAppleRemovableDrivesView.swift

@@ -67,8 +67,7 @@ struct VMAppleRemovableDrivesView: View {
                             })
                         }
                     } label: {
-                        Label { Text("Shared Directory") } icon: {
-                            Image(systemName: hasSharedDir ? "externaldrive.fill.badge.person.crop" : "externaldrive.badge.person.crop") }
+                        Label("Shared Directory", systemImage: hasSharedDir ? "externaldrive.fill.badge.person.crop" : "externaldrive.badge.person.crop")
                     }.disabled(vm.viewState.hasSaveState)
                     Spacer()
                     FilePath(url: sharedDirectory.directoryURL)

+ 3 - 1
Platform/macOS/VMConfigNetworkPortForwardView.swift

@@ -36,7 +36,9 @@ struct VMConfigNetworkPortForwardView: View {
             VStack {
                 ForEach(config.portForward) { forward in
                     Button(action: { selectedPortForward = forward }, label: {
-                        Text(verbatim: "\(forward.guestAddress ?? ""):\(forward.guestPort) ➡️ \(forward.hostAddress ?? ""):\(forward.hostPort)")
+                        let guest = "\(forward.guestAddress ?? ""):\(forward.guestPort)"
+                        let host = "\(forward.hostAddress ?? ""):\(forward.hostPort)"
+                        Text("\(guest) ➡️ \(host)")
                     }).buttonStyle(.bordered)
                     .popover(item: $selectedPortForward, arrowEdge: .bottom) { item in
                         PortForwardEdit(config: $config, forward: forward).padding()

+ 2 - 2
Platform/macOS/VMDrivesSettingsView.swift

@@ -104,10 +104,10 @@ struct VMDrivesSettingsView<Drive: UTMConfigurationDrive>: View {
             if qemuDrive.interface == .none && qemuDrive.imageName == QEMUPackageFileName.efiVariables.rawValue {
                 return NSLocalizedString("EFI Variables", comment: "VMDrivesSettingsView")
             } else {
-                return NSLocalizedString("\(qemuDrive.interface.prettyValue) Drive", comment: "VMDrivesSettingsView")
+                return String.localizedStringWithFormat(NSLocalizedString("%@ Drive", comment: "VMDrivesSettingsView"), qemuDrive.interface.prettyValue)
             }
         } else if let appleDrive = drive as? UTMAppleConfigurationDrive {
-            return NSLocalizedString("\(appleDrive.sizeString) Image", comment: "VMDrivesSettingsView")
+            return String.localizedStringWithFormat(NSLocalizedString("%@ Image", comment: "VMDrivesSettingsView"), appleDrive.sizeString)
         } else {
             fatalError("Unsupported drive type.")
         }

+ 16 - 0
UTM.xcodeproj/project.pbxproj

@@ -1190,6 +1190,9 @@
 		CED814EC24C7C2850042F0F1 /* VMConfigInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814EB24C7C2850042F0F1 /* VMConfigInfoView.swift */; };
 		CED814ED24C7C2850042F0F1 /* VMConfigInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814EB24C7C2850042F0F1 /* VMConfigInfoView.swift */; };
 		CED814EF24C7EB760042F0F1 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814EE24C7EB760042F0F1 /* ImagePicker.swift */; };
+		CED8DF7528A120C100C34345 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = CED8DF7928A120C100C34345 /* Localizable.stringsdict */; };
+		CED8DF7628A120C100C34345 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = CED8DF7928A120C100C34345 /* Localizable.stringsdict */; };
+		CED8DF7728A120C100C34345 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = CED8DF7928A120C100C34345 /* Localizable.stringsdict */; };
 		CEDF83F9258AE24E0030E4AC /* UTMPasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDF83F8258AE24E0030E4AC /* UTMPasteboard.swift */; };
 		CEDF83FA258AE24E0030E4AC /* UTMPasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDF83F8258AE24E0030E4AC /* UTMPasteboard.swift */; };
 		CEE7E936287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */; };
@@ -2150,6 +2153,7 @@
 		CED814E824C79F070042F0F1 /* VMConfigDriveCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigDriveCreateView.swift; sourceTree = "<group>"; };
 		CED814EB24C7C2850042F0F1 /* VMConfigInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigInfoView.swift; sourceTree = "<group>"; };
 		CED814EE24C7EB760042F0F1 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
+		CED8DF7828A120C100C34345 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
 		CEDC9BA2288B74E50030F494 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		CEDC9BA3288BBD130030F494 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
 		CEDC9BA4288BBD6B0030F494 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -2605,6 +2609,7 @@
 				CE2D955624AD4F980059923A /* Swift-Bridging-Header.h */,
 				CE550BD52259479D0063E575 /* Assets.xcassets */,
 				521F3EFB2414F73800130500 /* Localizable.strings */,
+				CED8DF7928A120C100C34345 /* Localizable.stringsdict */,
 				52459A312440C84E006A58D0 /* InfoPlist.strings */,
 			);
 			path = Platform;
@@ -3443,6 +3448,7 @@
 				CE2D936B24AD46670059923A /* Localizable.strings in Resources */,
 				CE2D936C24AD46670059923A /* qemu in Resources */,
 				CE2D936D24AD46670059923A /* VMDisplayMetalViewInputAccessory.xib in Resources */,
+				CED8DF7528A120C100C34345 /* Localizable.stringsdict in Resources */,
 				CE2D937224AD46670059923A /* Settings.bundle in Resources */,
 				CE0B6CED24AD532A00FE012D /* Assets.xcassets in Resources */,
 			);
@@ -3458,6 +3464,7 @@
 				CE2D959024AD50D50059923A /* Localizable.strings in Resources */,
 				FFB02A90266CB09C006CD71A /* InfoPlist.strings in Resources */,
 				CE0FE12824D3B08B0086CEF0 /* VMDisplayWindow.xib in Resources */,
+				CED8DF7728A120C100C34345 /* Localizable.stringsdict in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -3477,6 +3484,7 @@
 				CEA45F67263519B5002FA97D /* Localizable.strings in Resources */,
 				CEA45F69263519B5002FA97D /* qemu in Resources */,
 				CEA45F6A263519B5002FA97D /* VMDisplayMetalViewInputAccessory.xib in Resources */,
+				CED8DF7628A120C100C34345 /* Localizable.stringsdict in Resources */,
 				CEA45F6D263519B5002FA97D /* Settings.bundle in Resources */,
 				CEA45F6F263519B5002FA97D /* Assets.xcassets in Resources */,
 			);
@@ -4424,6 +4432,14 @@
 			name = InfoPlist.strings;
 			sourceTree = "<group>";
 		};
+		CED8DF7928A120C100C34345 /* Localizable.stringsdict */ = {
+			isa = PBXVariantGroup;
+			children = (
+				CED8DF7828A120C100C34345 /* en */,
+			);
+			name = Localizable.stringsdict;
+			sourceTree = "<group>";
+		};
 		FFB02A8A266CB09C006CD71A /* InfoPlist.strings */ = {
 			isa = PBXVariantGroup;
 			children = (