Reword user-facing errors, app info and Launchpad copy in plain written English

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lakr
2026-09-29 23:35:59 +09:00
co-authored by Claude Sonnet 5.5
parent 8682c98aaa
commit 090df08195
22 changed files with 225 additions and 49 deletions
@@ -276,7 +276,7 @@ enum GuestIrisinInstaller {
let packageData = try fetch(release.url, reportDownload: true)
let digest = SHA256.hash(data: packageData).map { String(format: "%02x", $0) }.joined()
guard digest == release.digest else {
throw GuestAPIError.operationFailed("Irisin release asset SHA-256 mismatch")
throw GuestAPIError.operationFailed("The download is damaged. Try again.")
}
try packageData.write(to: package, options: .atomic)
tag = release.tag
+1 -1
View File
@@ -19,7 +19,7 @@ extension GuestAPI {
return container + "/File Provider Storage"
}
}
throw GuestAPIError.operationFailed("The Files app storage container was not found")
throw GuestAPIError.operationFailed("Files app storage not found. Open Files once and try again.")
}
/// Moves an uploaded file into On My iPhone › vphone-drop. Items there are
+2 -1
View File
@@ -78,6 +78,7 @@ enum GuestAPI {
"screenshot",
"device_info",
"display",
"display_orientation",
"audio",
"input_gestures",
"ui_inspection",
@@ -346,7 +347,7 @@ enum GuestAPI {
let next = cache + ".next"
let data = try Data(contentsOf: URL(fileURLWithPath: next), options: .mappedIfSafe)
let actual = sha256Hex(data)
guard actual == expected else { throw GuestAPIError.invalidRequest("Update hash mismatch") }
guard actual == expected else { throw GuestAPIError.invalidRequest("The update is damaged. Try again.") }
guard chmod(next, 0o755) == 0 else {
throw GuestAPIError.operationFailed("Could not make update executable")
}
@@ -381,7 +381,7 @@ struct VPhoneCustomFirmwareInstaller {
let root = try work.directory.mountedVolume(name)
let source = try root.mountedFrom()
guard source == "/dev/\(device)" else {
throw ValidationError("Expected /dev/\(device) on \(work.file(name).path), found \(source). Try again.")
throw ValidationError("The VM disk image did not attach correctly. Try again.")
}
return root
}
@@ -33,7 +33,7 @@ private enum VPhoneVirtualMachineCreationError: Error, CustomStringConvertible {
case let .udidECIDMismatch(udid, ecid):
"The UDID and ECID in the device identity file do not match (\(udid), 0x\(ecid)). Run vm create again to regenerate it."
case .recoveryTimeout:
"Timed out waiting for the device to enter recovery mode."
"The device did not enter recovery mode in time. Run vm create again."
// No exit code any more: the restore backend is in this process, so
// what a failure carries is the reason it gave.
case let .restoreUpdateFailed(reason):
@@ -43,9 +43,9 @@ private enum VPhoneVirtualMachineCreationError: Error, CustomStringConvertible {
case .bootAnalysisPanic:
"Boot check failed: the guest panicked. Run vm create again."
case let .bootAnalysisExited(code):
"Boot check ended before vphoned connected (exit code \(code))."
"Boot check ended before the guest connected (exit code \(code)). Run vm create again."
case .bootAnalysisTimeout:
"Boot check timed out waiting for vphoned."
"Boot check timed out waiting for the guest. Run vm create again."
}
}
}
@@ -77,19 +77,19 @@ extension VPhoneRestoreBackendError: CustomStringConvertible {
public var description: String {
switch self {
case .ecidEmpty:
"ECID is empty"
"The ECID is empty. Enter up to 16 hexadecimal digits."
case let .ecidInvalid(value):
"Invalid ECID: \(value)"
"\(value) is not a valid ECID. Enter up to 16 hexadecimal digits."
case let .ecidTooLarge(value):
"ECID is too long: \(value). Enter up to 16 hexadecimal digits."
case let .noRestoreDirectory(dir):
"No iPhone*_Restore directory found in \(dir.path)"
"No restore folder was found in \(dir.path). Prepare the firmware, then try again."
case .multipleRestoreDirectories:
"Multiple iPhone*_Restore directories found; keep only one active restore tree"
"More than one restore folder was found. Keep only one, then try again."
case let .recoveryProbeTimedOut(mode):
"Timed out waiting for \(mode) endpoint"
"The device did not enter \(mode) mode in time. Check the connection, then try again."
case .recoveryDeviceUnreadable:
"The recovery endpoint stopped answering while it was being read"
"The device disconnected while it was being read. Reconnect it, then try again."
case .restoreAlreadyRunning:
"Another restore is already running. Wait for it to finish, then try again."
case let .restoreDirectoryUnusable(dir):
@@ -62,7 +62,7 @@ final class VPhoneAppBrowserModel {
case let .uninstalling(name): String(localized: "Uninstalling \(name)…", bundle: VPhoneLocalization.bundle)
case let .installing(file): String(localized: "Installing \(file)…", bundle: VPhoneLocalization.bundle)
case .openingURL: String(localized: "Opening URL…", bundle: VPhoneLocalization.bundle)
case .repairingNetwork: String(localized: "Repairing network policy…", bundle: VPhoneLocalization.bundle)
case .repairingNetwork: String(localized: "Allowing network access…", bundle: VPhoneLocalization.bundle)
}
}
}
@@ -449,11 +449,11 @@ final class VPhoneAppBrowserModel {
status = VPhoneGuestToolStatus(
message: allowed
? String(localized: "\(name) can use Wi-Fi and cellular data.", bundle: VPhoneLocalization.bundle)
: String(localized: "The network policy of \(name) did not change. Try again after relaunching the app.", bundle: VPhoneLocalization.bundle),
: String(localized: "Network access for \(name) did not change. Relaunch the app and try again.", bundle: VPhoneLocalization.bundle),
isError: !allowed,
)
} catch {
status = failure(String(localized: "Unable to repair the network policy of \(name).", bundle: VPhoneLocalization.bundle), error)
status = failure(String(localized: "Unable to allow network access for \(name).", bundle: VPhoneLocalization.bundle), error)
}
activity = nil
}
@@ -161,10 +161,10 @@ struct VPhoneAppInfoView: View {
}
private func networkSection(_ detail: VPhoneAppDetail) -> some View {
VPhoneAppInfoSection(title: "Network Policy", error: detail.networkPolicyError) {
VPhoneAppInfoSection(title: "Network Access", error: detail.networkPolicyError) {
if let policy = detail.networkPolicy {
Label {
Text(policy.allowed ? "Wi-Fi and cellular data allowed" : "Network access may be restricted")
Text(policy.allowed ? "Wi-Fi and cellular data allowed" : "Network access is restricted")
} icon: {
Image(systemName: policy.allowed ? "checkmark.circle.fill" : "exclamationmark.triangle.fill")
.foregroundStyle(policy.allowed ? .green : .orange)
@@ -173,7 +173,7 @@ struct VPhoneAppInfoView: View {
if !policy.entries.isEmpty {
VPhoneAppInfoGrid(rows: policy.entries.map { ($0.title, $0.value) }, localizeTitles: false)
}
Button("Repair") { Task { await model.repairNetworkPolicy() } }
Button("Allow Network") { Task { await model.repairNetworkPolicy() } }
.controlSize(.small)
.disabled(policy.allowed || model.isBusy || !model.control.isConnected)
.help("Allow Wi-Fi and cellular data for this app")
@@ -13,11 +13,11 @@ public enum VPhoneAPFSSnapshotError: Error, CustomStringConvertible {
case let .prefixLengthMismatch(given, required):
"--new-prefix must be exactly \(required) bytes (got \(given))"
case let .cannotOpen(url, err):
"Could not open \(url.path): \(String(cString: strerror(err)))"
"Unable to open \(url.path): \(String(cString: strerror(err))). Check the file and try again."
case let .cannotMap(url, err):
"Could not map \(url.path): \(String(cString: strerror(err)))"
"Unable to read \(url.path): \(String(cString: strerror(err))). Check the file and try again."
case let .cannotAccess(url, operation, err):
"Could not \(operation) \(url.path): \(String(cString: strerror(err)))"
"Unable to \(operation) \(url.path): \(String(cString: strerror(err))). Check the file and try again."
}
}
}
@@ -12,9 +12,9 @@ public enum VPhonePCCGPUDriver {
public var errorDescription: String? {
switch self {
case let .invalidBundle(path):
"GPU driver bundle is incomplete or has the wrong identifier: \(path.path)"
"The GPU driver at \(path.path) is incomplete or invalid. Download the firmware again and retry."
case let .wrongPlatformVersion(path, expected, actual):
"GPU driver at \(path.path) is for iPhoneOS \(actual), expected cloudOS \(expected)"
"The GPU driver at \(path.path) is for iPhoneOS \(actual), but cloudOS \(expected) is required. Choose a matching driver and try again."
}
}
}
@@ -26,7 +26,7 @@ extension VPhoneVirtualMachineManifest.PlatformFusing: ExpressibleByArgument {}
public struct VPhoneBootCommand: ParsableCommand {
public static let configuration = CommandConfiguration(
commandName: "boot",
abstract: "Boot a virtual iPhone (PV=3)",
abstract: "Boot a virtual iPhone",
discussion: """
Creates a Virtualization.framework VM with platform version 3 (vphone)
and boots it from a manifest plist that describes all paths and hardware.
@@ -111,7 +111,7 @@ public struct VPhoneBootCommand: ParsableCommand {
existingVariant != "jb"
{
throw ValidationError(
"This VM was created as '\(existingVariant)'. Only JB VMs are supported by this build.",
"This VM was created as '\(existingVariant)', which this build does not support. Create a new VM and try again.",
)
}
@@ -14,7 +14,7 @@ public enum VPhoneGuestBinaries {
switch self {
case let .missing(name, searched):
"""
Guest binary '\(name)' not found. Build the VPhone scheme in VPhone.xcworkspace.
\(name) not found. The app bundle is incomplete. Rebuild the VPhone scheme in Xcode.
Searched: \(searched.map(\.path).joined(separator: ", "))
"""
}
@@ -9,8 +9,8 @@ public enum VPhoneLibraryError: Error, Equatable {
extension VPhoneLibraryError: CustomStringConvertible, LocalizedError {
public var description: String {
switch self {
case let .notFound(name): "VM '\(name)' not found"
case let .alreadyExists(name): "VM '\(name)' already exists"
case let .notFound(name): "VM '\(name)' not found. Check the name and try again."
case let .alreadyExists(name): "VM '\(name)' already exists. Choose a different name."
case let .invalidName(name):
"Invalid VM name '\(name)'. Use a name that is not empty, has no '/', and does not start with '.'."
}
@@ -103,7 +103,7 @@ public struct VPhoneAPIClient: Sendable {
throw error
}
guard (200 ..< 300).contains(response.statusCode), let result = value.result else {
throw VPhoneAPIError(code: "protocol", message: "Missing result (HTTP \(response.statusCode))")
throw VPhoneAPIError(code: "protocol", message: "The VM returned an incomplete response. Try again.")
}
return result
}
@@ -138,7 +138,7 @@ public struct VPhoneAPIClient: Sendable {
let url = try fileURL(guestPath)
let (temporary, response) = try await session.download(for: authorized(URLRequest(url: url)))
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
throw VPhoneAPIError(code: "download", message: "Guest file download failed")
throw VPhoneAPIError(code: "download", message: "Unable to download the file from the VM. Try again.")
}
let destination = FileManager.default.temporaryDirectory
.appendingPathComponent("vphone-" + UUID().uuidString)
@@ -164,7 +164,7 @@ public struct VPhoneAPIClient: Sendable {
}
guard let response = response as? HTTPURLResponse,
response.statusCode == 200, let result = object["result"]
else { throw VPhoneAPIError(code: "upload", message: "Guest file upload failed") }
else { throw VPhoneAPIError(code: "upload", message: "Unable to upload the file to the VM. Try again.") }
return result
}
@@ -183,7 +183,7 @@ public struct VPhoneAPIClient: Sendable {
guard guestPath.hasPrefix("/"), !guestPath.contains("\0"),
var components = URLComponents(url: baseURL.appending(path: "v1/files/content"),
resolvingAgainstBaseURL: false)
else { throw VPhoneAPIError(code: "path", message: "Guest path must be absolute") }
else { throw VPhoneAPIError(code: "path", message: "The VM file path must start with '/'. Enter an absolute path.") }
components.queryItems = [URLQueryItem(name: "path", value: guestPath)]
if let mode {
components.queryItems?.append(URLQueryItem(name: "mode", value: mode))
@@ -581,7 +581,7 @@ struct VPhoneLaunchpadControlCommands {
onLine: emit,
)
guard status == 0 else {
throw VPhoneLaunchpadError("cfw install exited with status \(status).")
throw VPhoneLaunchpadError("Unable to install custom firmware. Check the log for details.")
}
return ["name": machine.name, "bundle": version, "status": status]
}
@@ -662,10 +662,10 @@ nonisolated enum VPhoneLaunchpadGuestSocket {
let line = try JSONSerialization.data(withJSONObject: object) + Data([0x0A])
let response = try await Task.detached { try exchange(line, socketPath: socketPath) }.value
guard let json = try? JSONSerialization.jsonObject(with: response) else {
throw VPhoneLaunchpadError("The machine answered with something other than JSON.", detail: String(decoding: response.prefix(512), as: UTF8.self))
throw VPhoneLaunchpadError("The machine sent a reply that could not be read. Try again.", detail: String(decoding: response.prefix(512), as: UTF8.self))
}
if let dictionary = json as? [String: Any], dictionary["ok"] as? Bool == false {
throw VPhoneLaunchpadError("The guest refused the request.", detail: dictionary["error"] as? String)
throw VPhoneLaunchpadError("The machine refused the request. Try again.", detail: dictionary["error"] as? String)
}
return json
}
@@ -697,7 +697,7 @@ nonisolated enum VPhoneLaunchpadGuestSocket {
}
}
guard connected == 0 else {
throw VPhoneLaunchpadError("The machine's vphone.sock is not answering.", detail: String(cString: strerror(errno)))
throw VPhoneLaunchpadError("The machine is not responding. Make sure it is running, then try again.", detail: String(cString: strerror(errno)))
}
guard VPhoneLaunchpadControl.write(line, to: fd) else {
throw VPhoneLaunchpadError("Unable to send the request to the machine.")
@@ -44,7 +44,7 @@ final class VPhoneLaunchpadCoreBundle {
case .prepare: String(localized: "Prepare bundle")
case .download: String(localized: "Download")
case .verify: String(localized: "Verify SHA-256")
case .install: String(localized: "Install as root")
case .install: String(localized: "Install with administrator access")
case .policy: String(localized: "Allow bundle to run")
case .preflight: String(localized: "Host preflight")
}
@@ -91,7 +91,7 @@ struct VPhoneLaunchpadCoreBundleView: View {
} header: {
Text("Installed")
} footer: {
Text("Stored in \(VPhoneLaunchpadBundleStore.root.path), owned by root and written only by the helper.")
Text("Stored in \(VPhoneLaunchpadBundleStore.root.path) and managed by the helper.")
.foregroundStyle(.secondary)
}
}
@@ -10,6 +10,30 @@
"state" : "new",
"value" : "vphone-launchpad"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "vphone-launchpad"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "vphone-launchpad"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "vphone-launchpad"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "vphone-launchpad"
}
}
}
},
@@ -22,6 +46,30 @@
"state" : "new",
"value" : "vphone-launchpad"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "vphone-launchpad"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "vphone-launchpad"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "vphone-launchpad"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "vphone-launchpad"
}
}
}
},
@@ -1,9 +1,6 @@
{
"sourceLanguage" : "en",
"strings" : {
"" : {
},
"%@ · %@ · Expires %@" : {
"localizations" : {
"en" : {
@@ -1448,6 +1445,26 @@
}
}
},
"Allow Network" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Allow Network"
}
}
}
},
"Allowing network access…" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Allowing network access…"
}
}
}
},
"Allowed" : {
"localizations" : {
"en" : {
@@ -4463,6 +4480,26 @@
}
}
},
"Every patch in this bundle, including advanced ones that a freshly restored 26.4 guest may not survive." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Every patch in this bundle, including advanced ones that a freshly restored 26.4 guest may not survive."
}
}
}
},
"Every patch in this bundle, including advanced ones that may stop a newly restored 26.4 machine from starting." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Every patch in this bundle, including advanced ones that may stop a newly restored 26.4 machine from starting."
}
}
}
},
"Every patch this bundle declares, including the Frida Stalker relaxations and the hv_vmm_present concealment that a freshly restored 26.4 guest does not survive." : {
"localizations" : {
"en" : {
@@ -5998,6 +6035,26 @@
}
}
},
"Install custom firmware" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Install custom firmware"
}
}
}
},
"Install with administrator access" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Install with administrator access"
}
}
}
},
"Install CFW" : {
"localizations" : {
"en" : {
@@ -7398,6 +7455,36 @@
}
}
},
"Network Access" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Network Access"
}
}
}
},
"Network access is restricted" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Network access is restricted"
}
}
}
},
"Network access for %@ did not change. Relaunch the app and try again." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Network access for %@ did not change. Relaunch the app and try again."
}
}
}
},
"Network" : {
"localizations" : {
"en" : {
@@ -8658,6 +8745,16 @@
}
}
},
"Patches every machine needs to start, with a working display and camera." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Patches every machine needs to start, with a working display and camera."
}
}
}
},
"Patches" : {
"localizations" : {
"en" : {
@@ -11632,6 +11729,16 @@
}
}
},
"Stored in %@ and managed by the helper." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Stored in %@ and managed by the helper."
}
}
}
},
"Stored in %@, owned by root and written only by the helper." : {
"localizations" : {
"en" : {
@@ -12905,6 +13012,26 @@
}
}
},
"Unable to allow network access for %@." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unable to allow network access for %@."
}
}
}
},
"Unable to install custom firmware. Check the log for details." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unable to install custom firmware. Check the log for details."
}
}
}
},
"Unable to Complete Action" : {
"localizations" : {
"en" : {
@@ -62,7 +62,7 @@ final class VPhoneLaunchpadCreationPipeline {
case .waitDFU: String(localized: "Wait for DFU")
case .restore: String(localized: "Restore")
case .stopDFU: String(localized: "Stop machine")
case .installCFW: String(localized: "Install CFW")
case .installCFW: String(localized: "Install custom firmware")
case .firstBoot: String(localized: "First boot")
}
}
@@ -355,7 +355,7 @@ final class VPhoneLaunchpadCreationPipeline {
onLine: output,
)
guard status == 0 else {
throw VPhoneLaunchpadError(String(localized: "Unable to install CFW. Check the log for details."), detail: log.tail)
throw VPhoneLaunchpadError(String(localized: "Unable to install custom firmware. Check the log for details."), detail: log.tail)
}
case .firstBoot:
@@ -29,8 +29,8 @@ nonisolated struct VPhoneLaunchpadPatchCatalog: Decodable, Sendable {
var displaySummary: String {
switch identifier {
case "standard": String(localized: "The patches every vphone VM needs to boot, jailbroken, with a working display and camera.")
case "extended": String(localized: "Every patch this bundle declares, including the Frida Stalker relaxations and the hv_vmm_present concealment that a freshly restored 26.4 guest does not survive.")
case "standard": String(localized: "Patches every machine needs to start, with a working display and camera.")
case "extended": String(localized: "Every patch in this bundle, including advanced ones that may stop a newly restored 26.4 machine from starting.")
default: summary
}
}
@@ -31,7 +31,7 @@ enum VPhoneLaunchpadHelperAMFI {
info.st_uid == 0,
info.st_mode & 0o022 == 0
else {
throw VPhoneLaunchpadHelperError("vphone-escalator is missing or is not root-owned. Reinstall VPhone.bundle.")
throw VPhoneLaunchpadHelperError("A required tool in VPhone.bundle is missing or was changed. Reinstall VPhone.bundle.")
}
let process = Process()
@@ -44,13 +44,13 @@ enum VPhoneLaunchpadHelperAMFI {
do {
try process.run()
} catch {
throw VPhoneLaunchpadHelperError("Unable to start vphone-escalator: \(error.localizedDescription)")
throw VPhoneLaunchpadHelperError("Unable to allow the bundle to run. Try again.")
}
let text = String(decoding: output.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
.trimmingCharacters(in: .whitespacesAndNewlines)
process.waitUntilExit()
guard process.terminationReason == .exit, process.terminationStatus == 0 else {
throw VPhoneLaunchpadHelperError(text.isEmpty ? "vphone-escalator failed. Check the host's SIP settings." : text)
throw VPhoneLaunchpadHelperError(text.isEmpty ? "Unable to allow the bundle to run. Check the System Integrity Protection settings on this Mac." : text)
}
}
}