convert strings to support Localization

This commit is contained in:
Matt Kiazyk 2022-04-14 23:18:42 -05:00
parent 03a136385f
commit 1d22be649a
No known key found for this signature in database
GPG key ID: 850581D2373E4A99
29 changed files with 389 additions and 179 deletions

View file

@ -102,6 +102,7 @@
CAFE4ABC25B7D54B0064FE51 /* UpdatesPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */; };
CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */; };
E81D7EA02805250100A205FC /* Collection+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81D7E9F2805250100A205FC /* Collection+.swift */; };
E872EE4E2808D4F100D3DD8B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E872EE502808D4F100D3DD8B /* Localizable.strings */; };
E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; };
E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E89342F925EDCC17007CF557 /* NotificationManager.swift */; };
E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8977EA225C11E1500835F80 /* PreferencesView.swift */; };
@ -274,6 +275,7 @@
CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListViewRow.swift; sourceTree = "<group>"; };
CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = "<group>"; };
E81D7E9F2805250100A205FC /* Collection+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+.swift"; sourceTree = "<group>"; };
E872EE4F2808D4F100D3DD8B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
E87DD6EA25D053FA00D86808 /* Progress+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Progress+.swift"; sourceTree = "<group>"; };
E89342F925EDCC17007CF557 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
E8977EA225C11E1500835F80 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
@ -493,6 +495,7 @@
CAD2E7AE2449575000113D76 /* Xcodes.entitlements */,
CA8FB64D256E17B100469DA5 /* XcodesTest.entitlements */,
E8CBDB8627ADD92000B22292 /* unxip */,
E872EE502808D4F100D3DD8B /* Localizable.strings */,
);
path = Resources;
sourceTree = "<group>";
@ -715,6 +718,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E872EE4E2808D4F100D3DD8B /* Localizable.strings in Resources */,
CAD2E7A92449575000113D76 /* Preview Assets.xcassets in Resources */,
CA9FF83F2594FBC000E47BAF /* Licenses.rtf in Resources */,
CAA858DB25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz in Resources */,
@ -878,6 +882,17 @@
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
E872EE502808D4F100D3DD8B /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
E872EE4F2808D4F100D3DD8B /* en */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
CA8FB635256E154800469DA5 /* Test */ = {
isa = XCBuildConfiguration;
@ -936,6 +951,7 @@
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Test;
@ -1120,6 +1136,7 @@
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
@ -1174,6 +1191,7 @@
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;

View file

@ -215,7 +215,7 @@ extension AppState {
.handleEvents(receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
self.presentedAlert = .generic(title: "Unable to install archived Xcode", message: error.legibleLocalizedDescription)
self.presentedAlert = .generic(title: localizeString("Alert.InstallArchive.Error.Title"), message: error.legibleLocalizedDescription)
}
})
.catch { _ in
@ -362,7 +362,7 @@ extension AppState {
receiveCompletion: { completion in
if case let .failure(error) = completion {
self.error = error
self.presentedAlert = .generic(title: "Unable to perform post install steps", message: error.legibleLocalizedDescription)
self.presentedAlert = .generic(title: localizeString("Alert.PostInstall.Title"), message: error.legibleLocalizedDescription)
}
},
receiveValue: {}
@ -503,60 +503,41 @@ public enum InstallationError: LocalizedError, Equatable {
public var errorDescription: String? {
switch self {
case .damagedXIP(let url):
return "The archive \"\(url.lastPathComponent)\" is damaged and can't be expanded."
return String(format: localizeString("InstallationError.DamagedXIP"), url.lastPathComponent)
case let .notEnoughFreeSpaceToExpandArchive(archivePath, version):
return """
The archive \(archivePath.basename()) cant be expanded because the current volume doesnt have enough free space.
Make more space available to expand the archive and then install Xcode \(version.appleDescription) again to start installation from where it left off.
"""
return String(format: localizeString("InstallationError.NotEnoughFreeSpaceToExpandArchive"), archivePath.basename(), version.appleDescription)
case .failedToMoveXcodeToApplications:
return "Failed to move Xcode to the /Applications directory."
return String(format: localizeString("InstallationError.FailedToMoveXcodeToApplications"), Path.installDirectory.string)
case .failedSecurityAssessment(let xcode, let output):
return """
Xcode \(xcode.version) failed its security assessment with the following output:
\(output)
It remains installed at \(xcode.path) if you wish to use it anyways.
"""
return String(format: localizeString("InstallationError.FailedSecurityAssessment"), String(xcode.version), output, xcode.path.string)
case .codesignVerifyFailed(let output):
return """
The downloaded Xcode failed code signing verification with the following output:
\(output)
"""
return String(format: localizeString("InstallationError.CodesignVerifyFailed"), output)
case .unexpectedCodeSigningIdentity(let identity, let certificateAuthority):
return """
The downloaded Xcode doesn't have the expected code signing identity.
Got:
\(identity)
\(certificateAuthority)
Expected:
\(XcodeTeamIdentifier)
\(XcodeCertificateAuthority)
"""
return String(format: localizeString("InstallationError.UnexpectedCodeSigningIdentity"), identity, certificateAuthority, XcodeTeamIdentifier, XcodeCertificateAuthority)
case .unsupportedFileFormat(let fileExtension):
return "xcodes doesn't (yet) support installing Xcode from the \(fileExtension) file format."
return String(format: localizeString("InstallationError.UnsuppoawwrtedFileFormat"), fileExtension)
case .missingSudoerPassword:
return "Missing password. Please try again."
return localizeString("InstallationError.MissingSudoerPassword")
case let .unavailableVersion(version):
return "Could not find version \(version.appleDescription)."
return String(format: localizeString("InstallationError.UnavailableVersion"), version.appleDescription)
case .noNonPrereleaseVersionAvailable:
return "No non-prerelease versions available."
return localizeString("InstallationError.NoNonPrereleaseVersionAvailable")
case .noPrereleaseVersionAvailable:
return "No prerelease versions available."
return localizeString("InstallationError.NoPrereleaseVersionAvailable")
case .missingUsernameOrPassword:
return "Missing username or a password. Please try again."
return localizeString("InstallationError.MissingUsernameOrPassword")
case let .versionAlreadyInstalled(installedXcode):
return "\(installedXcode.version.appleDescription) is already installed at \(installedXcode.path)"
return String(format: localizeString("InstallationError.VersionAlreadyInstalled"), installedXcode.version.appleDescription, installedXcode.path.string)
case let .invalidVersion(version):
return "\(version) is not a valid version number."
return String(format: localizeString("InstallationError.InvalidVersion"), version)
case let .versionNotInstalled(version):
return "\(version.appleDescription) is not installed."
return String(format: localizeString("InstallationError.VersionNotInstalled"), version.appleDescription)
case let .postInstallStepsNotPerformed(version, helperInstallState):
switch helperInstallState {
case .installed:
return "Installation was completed, but some post-install steps weren't performed automatically. These will be performed when you first launch Xcode \(version.appleDescription)."
return String(format: localizeString("InstallationError.PostInstallStepsNotPerformed.Installed"), version.appleDescription)
case .notInstalled, .unknown:
return "Installation was completed, but some post-install steps weren't performed automatically. Xcodes performs these steps with a privileged helper, which appears to not be installed. You can install it from Preferences > Advanced.\n\nThese steps will be performed when you first launch Xcode \(version.appleDescription)."
return String(format: localizeString("InstallationError.PostInstallStepsNotPerformed.NotInstalled"), version.appleDescription)
}
}
}

View file

@ -47,7 +47,7 @@ extension AppState {
// Prevent setting the app state error if it is an invalid session, we will present the sign in view instead
if error as? AuthenticationError != .invalidSession {
self.error = error
self.presentedAlert = .generic(title: "Unable to update selected Xcode", message: error.legibleLocalizedDescription)
self.presentedAlert = .generic(title: localizeString("Alert.Update.Error.Title"), message: error.legibleLocalizedDescription)
}
case .finished:
Current.defaults.setDate(Current.date(), forKey: "lastUpdated")
@ -141,7 +141,7 @@ extension AppState {
throw AuthenticationError.invalidResult(resultString: downloads.resultsString)
}
guard let downloadList = downloads.downloads else {
throw AuthenticationError.invalidResult(resultString: "No download information found")
throw AuthenticationError.invalidResult(resultString: localizeString("DownloadingError"))
}
let xcodes = downloadList
.filter { $0.name.range(of: "^Xcode [0-9]", options: .regularExpression) != nil }

View file

@ -17,7 +17,7 @@ class AppState: ObservableObject {
@Published var availableXcodes: [AvailableXcode] = [] {
willSet {
if newValue.count > availableXcodes.count && availableXcodes.count != 0 {
Current.notificationManager.scheduleNotification(title: "New Xcode versions", body: "New Xcode versions are available to download.", category: .normal)
Current.notificationManager.scheduleNotification(title: localizeString("Notification.NewXcodeVersion.Title"), body: localizeString("Notification.NewXcodeVersion.Body"), category: .normal)
}
updateAllXcodes(
availableXcodes: newValue,
@ -283,7 +283,7 @@ class AppState: ObservableObject {
receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
self.presentedAlert = .generic(title: "Unable to install helper", message: error.legibleLocalizedDescription)
self.presentedAlert = .generic(title: localizeString("Alert.PrivilegedHelper.Error.Title"), message: error.legibleLocalizedDescription)
}
},
receiveValue: {}
@ -377,7 +377,7 @@ class AppState: ObservableObject {
throw AuthenticationError.invalidResult(resultString: downloads.resultsString)
}
if downloads.downloads == nil {
throw AuthenticationError.invalidResult(resultString: "No download information found")
throw AuthenticationError.invalidResult(resultString: localizeString("DownloadingError"))
}
}
.mapError { $0 as Error }
@ -393,7 +393,7 @@ class AppState: ObservableObject {
// Prevent setting the app state error if it is an invalid session, we will present the sign in view instead
if error as? AuthenticationError != .invalidSession {
self.error = error
self.presentedAlert = .generic(title: "Unable to install Xcode", message: error.legibleLocalizedDescription)
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
}
if let index = self.allXcodes.firstIndex(where: { $0.id == id }) {
self.allXcodes[index].installState = .notInstalled
@ -437,7 +437,7 @@ class AppState: ObservableObject {
receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
self.presentedAlert = .generic(title: "Unable to uninstall Xcode", message: error.legibleLocalizedDescription)
self.presentedAlert = .generic(title: localizeString("Alert.Uninstall.Error.Title"), message: error.legibleLocalizedDescription)
}
self.uninstallPublisher = nil
},
@ -494,7 +494,7 @@ class AppState: ObservableObject {
receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
self.presentedAlert = .generic(title: "Unable to select Xcode", message: error.legibleLocalizedDescription)
self.presentedAlert = .generic(title: localizeString("Alert.Select.Error.Title"), message: error.legibleLocalizedDescription)
} else {
if self.createSymLinkOnSelect {
createSymbolicLink(xcode: xcode)
@ -539,12 +539,11 @@ class AppState: ObservableObject {
try FileManager.default.removeItem(atPath: destinationPath.string)
Logger.appState.info("Successfully deleted old symlink")
} else {
let message = "Xcode.app exists and is not a symbolic link"
self.presentedAlert = .generic(title: "Unable to create symbolic Link", message: message)
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: localizeString("Alert.SymLink.Message"))
return
}
} catch {
self.presentedAlert = .generic(title: "Unable to create symbolic Link", message: error.localizedDescription)
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.localizedDescription)
}
}
@ -554,7 +553,7 @@ class AppState: ObservableObject {
} catch {
Logger.appState.error("Unable to create symbolic Link")
self.error = error
self.presentedAlert = .generic(title: "Unable to create symbolic Link", message: error.legibleLocalizedDescription)
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
}
}

View file

@ -361,7 +361,7 @@ enum HelperClientError: LocalizedError {
var errorDescription: String? {
switch self {
case .failedToCreateRemoteObjectProxy:
return "Unable to communicate with privileged helper."
return localizeString("HelperClient.error")
case let .message(message):
return message
}

View file

@ -16,17 +16,17 @@ enum InstallationStep: Equatable, CustomStringConvertible {
var message: String {
switch self {
case .downloading:
return "Downloading"
return localizeString("Downloading")
case .unarchiving:
return "Unarchiving (This can take a while)"
return localizeString("Unarchiving")
case .moving(let destination):
return "Moving to \(destination)"
return String(format: localizeString("Moving"), destination)
case .trashingArchive:
return "Moving archive to the Trash"
return localizeString("TrashingArchive")
case .checkingSecurity:
return "Security verification"
return localizeString("CheckingSecurity")
case .finishing:
return "Finishing"
return localizeString("Finishing")
}
}

View file

@ -22,9 +22,9 @@ public enum XcodesNotificationType: String, Identifiable, CaseIterable, CustomSt
public var description: String {
switch self {
case .newVersionAvailable:
return "New version is available"
return localizeString("Notification.NewVersionAvailable")
case .finishedInstalling:
return "Finished installing"
return localizeString("Notification.FinishedInstalling")
}
}
}

View file

@ -56,7 +56,7 @@ struct CancelInstallButton: View {
var body: some View {
Button(action: cancelInstall) {
Text("Cancel")
.help("Stop installation")
.help(localizeString("StopInstallation"))
}
}
@ -75,7 +75,7 @@ struct SelectButton: View {
if xcode?.selected == true {
Text("Active")
} else {
Text("Make active")
Text("MakeActive")
}
}
.disabled(xcode?.selected != false)
@ -126,9 +126,9 @@ struct RevealButton: View {
var body: some View {
Button(action: reveal) {
Text("Reveal in Finder")
Text("RevealInFinder")
}
.help("Reveal in Finder")
.help("RevealInFinder")
}
private func reveal() {
@ -143,9 +143,9 @@ struct CopyPathButton: View {
var body: some View {
Button(action: copyPath) {
Text("Copy Path")
Text("CopyPath")
}
.help("Copy path")
.help("CopyPath")
}
private func copyPath() {
@ -160,9 +160,9 @@ struct CreateSymbolicLinkButton: View {
var body: some View {
Button(action: createSymbolicLink) {
Text("Create SymLink as Xcode.app")
Text("CreateSymLink")
}
.help("Create SymLink as Xcode.app")
.help("CreateSymLink")
}
private func createSymbolicLink() {

View file

@ -12,13 +12,13 @@ struct AboutView: View {
Text(Bundle.main.bundleName!)
.font(.largeTitle)
Text("Version \(Bundle.main.shortVersion!) (\(Bundle.main.version!))")
Text(String(format: localizeString("VersionWithBuild"), Bundle.main.shortVersion!, Bundle.main.version!))
HStack(spacing: 32) {
Button(action: {
openURL(URL(string: "https://github.com/RobotsAndPencils/XcodesApp/")!)
}) {
Label("GitHub Repo", systemImage: "link")
Label("GitHubRepo", systemImage: "link")
}
.buttonStyle(LinkButtonStyle())
@ -29,12 +29,12 @@ struct AboutView: View {
}
Color.clear
.frame(width: 300, height: 0)
Label("Unxip Experiment", systemImage: "lightbulb")
Label("UnxipExperiment", systemImage: "lightbulb")
HStack(spacing: 32) {
Button(action: {
openURL(URL(string: "https://github.com/saagarjha/unxip/")!)
}) {
Label("Github Repo", systemImage: "link")
Label("GithubRepo", systemImage: "link")
}
.buttonStyle(LinkButtonStyle())

View file

@ -46,7 +46,7 @@ public struct ObservingProgressIndicator: View {
isIndeterminate: progress.progress.isIndeterminate,
style: style
)
.help("Downloading: \(Int((progress.progress.fractionCompleted * 100)))% complete")
.help(String(format: localizeString("DownloadingPercentDescription"), Int((progress.progress.fractionCompleted * 100))))
if showsAdditionalDescription, progress.progress.xcodesLocalizedDescription.isEmpty == false {
Text(progress.progress.xcodesLocalizedDescription)

View file

@ -35,7 +35,7 @@ struct InfoPane: View {
Image(systemName: "arrow.right.circle.fill")
}
.buttonStyle(PlainButtonStyle())
.help("Reveal in Finder")
.help("RevealInFinder")
}
HStack {
@ -90,11 +90,11 @@ struct InfoPane: View {
if !xcode.identicalBuilds.isEmpty {
VStack(alignment: .leading) {
HStack {
Text("Identical Builds")
Text("IdenticalBuilds")
Image(systemName: "square.fill.on.square.fill")
.foregroundColor(.secondary)
.accessibility(hidden: true)
.help("Sometimes a prerelease and release version are the exact same build. Xcodes will automatically display these versions together.")
.help("IdenticalBuilds.help")
}
.font(.headline)
@ -105,9 +105,9 @@ struct InfoPane: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityElement()
.accessibility(label: Text("Identical Builds"))
.accessibility(label: Text("IdenticalBuilds"))
.accessibility(value: Text(xcode.identicalBuilds.map(\.appleDescription).joined(separator: ", ")))
.accessibility(hint: Text("Sometimes a prerelease and release version are the exact same build. Xcodes will automatically display these versions together."))
.accessibility(hint: Text("IdenticalBuilds.help"))
} else {
EmptyView()
}
@ -117,7 +117,7 @@ struct InfoPane: View {
private func releaseDate(for xcode: Xcode) -> some View {
if let releaseDate = xcode.releaseDate {
VStack(alignment: .leading) {
Text("Release Date")
Text("ReleaseDate")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
Text(DateFormatter.downloadsReleaseDate.string(from: releaseDate))
@ -133,11 +133,11 @@ struct InfoPane: View {
private func releaseNotes(for xcode: Xcode) -> some View {
if let releaseNotesURL = xcode.releaseNotesURL {
Button(action: { openURL(releaseNotesURL) }) {
Label("Release Notes", systemImage: "link")
Label("ReleaseNotes", systemImage: "link")
}
.buttonStyle(LinkButtonStyle())
.frame(maxWidth: .infinity, alignment: .leading)
.help("View Release Notes")
.help("ReleaseNotes.help")
} else {
EmptyView()
}
@ -150,7 +150,7 @@ struct InfoPane: View {
Text("Compatibility")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
Text("Requires macOS \(requiredMacOSVersion) or later")
Text(String(format: localizeString("MacOSRequirement"), requiredMacOSVersion))
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .leading)
}
@ -217,7 +217,7 @@ struct InfoPane: View {
// if we've downloaded it no need to show the download size
if let downloadFileSize = xcode.downloadFileSizeString, case .notInstalled = xcode.installState {
VStack(alignment: .leading) {
Text("Download Size")
Text("DownloadSize")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
Text("\(downloadFileSize)")
@ -231,7 +231,7 @@ struct InfoPane: View {
@ViewBuilder
private var empty: some View {
Text("No Xcode Selected")
Text("NoXcodeSelected")
.font(.title)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)

View file

@ -5,7 +5,7 @@ struct InstallationStepDetailView: View {
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text("Step \(installationStep.stepNumber) of \(installationStep.stepCount): \(installationStep.message)")
Text(String(format: localizeString("InstallationStepDescription"), installationStep.stepNumber, installationStep.stepCount, installationStep.message))
switch installationStep {
case let .downloading(progress):

View file

@ -20,8 +20,8 @@ struct MainWindow: View {
.frame(minWidth: 300)
.layoutPriority(1)
.alert(item: $appState.xcodeBeingConfirmedForUninstallation) { xcode in
Alert(title: Text("Uninstall Xcode \(xcode.description)?"),
message: Text("It will be moved to the Trash, but won't be emptied."),
Alert(title: Text(String(format: localizeString("Alert.Uninstall.Title"), xcode.description)),
message: Text("Alert.Uninstall.Message"),
primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(xcode: xcode) }),
secondaryButton: .cancel(Text("Cancel")))
}
@ -60,7 +60,7 @@ struct MainWindow: View {
private var subtitleText: Text {
if let lastUpdated = lastUpdated.map(Date.init(timeIntervalSince1970:)) {
return Text("Updated at \(lastUpdated, style: .date) \(lastUpdated, style: .time)")
return Text("\(localizeString("UpdatedAt")) \(lastUpdated, style: .date) \(lastUpdated, style: .time)")
} else {
return Text("")
}
@ -101,10 +101,10 @@ struct MainWindow: View {
switch alertType {
case let .cancelInstall(xcode):
return Alert(
title: Text("Are you sure you want to stop the installation of Xcode \(xcode.description)?"),
message: Text("Any progress will be discarded."),
title: Text(String(format: "Alert.CancelInstall.Title", xcode.description)),
message: Text("Alert.CancelInstall.Message"),
primaryButton: .destructive(
Text("Stop Installation"),
Text("Alert.CancelInstall.PrimaryButton"),
action: {
self.appState.cancelInstall(id: xcode.id)
}
@ -113,8 +113,8 @@ struct MainWindow: View {
)
case .privilegedHelper:
return Alert(
title: Text("Privileged Helper"),
message: Text("Xcodes uses a separate privileged helper to perform tasks as root. These are things that would require sudo on the command line, including post-install steps and switching Xcode versions with xcode-select.\n\nYou'll be prompted for your macOS account password to install it."),
title: Text("Alert.PrivilegedHelper.Title"),
message: Text("Alert.PrivilegedHelper.Message"),
primaryButton: .default(Text("Install"), action: {
// The isPreparingUserForActionRequiringHelper closure is set to nil by the alert's binding when its dismissed.
// We need to capture it to be invoked after that happens.
@ -153,8 +153,8 @@ struct MainWindow: View {
)
case let .checkMinSupportedVersion(xcode, deviceVersion):
return Alert(
title: Text("Minimum requirements not met"),
message: Text("Xcode \(xcode.version.descriptionWithoutBuildMetadata) requires MacOS \(xcode.requiredMacOSVersion ?? ""), but you are running MacOS \(deviceVersion), do you still want to install it?"),
title: Text("Alert.MinSupported.Title"),
message: Text(String(format: "Alert.MinSupported.Message", xcode.version.descriptionWithoutBuildMetadata, xcode.requiredMacOSVersion ?? "", deviceVersion)),
primaryButton: .default(
Text("Install"),
action: {

View file

@ -11,7 +11,7 @@ struct AdvancedPreferencePane: View {
var body: some View {
VStack(alignment: .leading, spacing: 20) {
GroupBox(label: Text("Local Cache Path")) {
GroupBox(label: Text("LocalCachePath")) {
VStack(alignment: .leading) {
HStack(alignment: .top, spacing: 5) {
Text(appState.localPath).font(.footnote)
@ -39,7 +39,7 @@ struct AdvancedPreferencePane: View {
self.appState.localPath = path.string
}
}
Text("Xcodes caches available Xcode versions and temporary downloads new versions to a directory")
Text("LocalCachePathDescription")
.font(.footnote)
.fixedSize(horizontal: false, vertical: true)
}
@ -49,10 +49,10 @@ struct AdvancedPreferencePane: View {
GroupBox(label: Text("Active/Select")) {
VStack(alignment: .leading) {
Toggle(
"Automatically create symbolic link to Xcodes.app",
"AutomaticallyCreateSymbolicLink",
isOn: $appState.createSymLinkOnSelect
)
Text("When making an Xcode version Active/Selected, try and create a symbolic link named Xcode.app in the installation directory")
Text("AutomaticallyCreateSymbolicLinkDescription")
.font(.footnote)
.fixedSize(horizontal: false, vertical: true)
}
@ -60,9 +60,9 @@ struct AdvancedPreferencePane: View {
}
.groupBoxStyle(PreferencesGroupBoxStyle())
GroupBox(label: Text("Data Source")) {
GroupBox(label: Text("DataSource")) {
VStack(alignment: .leading) {
Picker("Data Source", selection: $dataSource) {
Picker("DataSource", selection: $dataSource) {
ForEach(DataSource.allCases) { dataSource in
Text(dataSource.description)
.tag(dataSource)
@ -92,24 +92,24 @@ struct AdvancedPreferencePane: View {
}
.groupBoxStyle(PreferencesGroupBoxStyle())
GroupBox(label: Text("Privileged Helper")) {
GroupBox(label: Text("PrivilegedHelper")) {
VStack(alignment: .leading, spacing: 8) {
switch appState.helperInstallState {
case .unknown:
ProgressView()
.scaleEffect(0.5, anchor: .center)
case .installed:
Text("Helper is installed")
Text("HelperInstalled")
case .notInstalled:
HStack {
Text("Helper is not installed")
Button("Install helper") {
Text("HelperNotInstalled")
Button("InstallHelper") {
appState.installHelperIfNecessary()
}
}
}
Text("Xcodes uses a separate privileged helper to perform tasks as root. These are things that would require sudo on the command line, including post-install steps and switching Xcode versions with xcode-select.\n\nYou'll be prompted for your macOS account password to install it.")
Text("PrivilegedHelperDescription")
.font(.footnote)
.fixedSize(horizontal: false, vertical: true)
@ -122,11 +122,7 @@ struct AdvancedPreferencePane: View {
}
private var dataSourceFootnote: NSAttributedString {
let string = """
The Apple data source scrapes the Apple Developer website. It will always show the latest releases that are available, but is more fragile.
Xcode Releases is an unofficial list of Xcode releases. It's provided as well-formed data, contains extra information that is not readily available from Apple, and is less likely to break if Apple redesigns their developer website.
"""
let string = localizeString("DataSourceDescription")
let attributedString = NSMutableAttributedString(
string: string,
attributes: [
@ -139,11 +135,7 @@ struct AdvancedPreferencePane: View {
}
private var downloaderFootnote: NSAttributedString {
let string = """
aria2 uses up to 16 connections to download Xcode 3-5x faster than URLSession. It's bundled as an executable along with its source code within Xcodes to comply with its GPLv2 license.
URLSession is the default Apple API for making URL requests.
"""
let string = localizeString("DownloaderDescription")
let attributedString = NSMutableAttributedString(
string: string,
attributes: [

View file

@ -7,10 +7,10 @@ struct ExperimentsPreferencePane: View {
var body: some View {
VStack(alignment: .leading, spacing: 20) {
GroupBox(label: Text("Faster Unxip")) {
GroupBox(label: Text("FasterUnxip")) {
VStack(alignment: .leading) {
Toggle(
"When unxipping, use experiment",
"UseUnxipExperiment",
isOn: $appState.unxipExperiment
)
AttributedText(unxipFootnote)
@ -25,13 +25,7 @@ struct ExperimentsPreferencePane: View {
}
private var unxipFootnote: NSAttributedString {
let string = """
Thanks to @_saagarjha, this experiment can increase unxipping
speed by up to 70% for some systems.
More information on how this is accomplished can be seen
on the unxip repo - https://github.com/saagarjha/unxip
"""
let string = localizeString("FasterUnxipDescription")
let attributedString = NSMutableAttributedString(
string: string,
attributes: [

View file

@ -6,11 +6,11 @@ struct GeneralPreferencePane: View {
var body: some View {
VStack(alignment: .leading) {
GroupBox(label: Text("Apple ID")) {
GroupBox(label: Text("AppleID")) {
if appState.authenticationState == .authenticated {
SignedInView()
} else {
Button("Sign In", action: { self.appState.presentedSheet = .signIn })
Button("SignIn", action: { self.appState.presentedSheet = .signIn })
}
}
.groupBoxStyle(PreferencesGroupBoxStyle())

View file

@ -8,17 +8,17 @@ struct NotificationsView: View {
switch Current.notificationManager.notificationStatus {
case .shownAndAccepted:
Text("Access Granted. You will receive notifications from Xcodes.")
Text("AccessGranted")
.fixedSize(horizontal: false, vertical: true)
case .shownAndDenied:
Text("⚠️ Access Denied ⚠️\n\nPlease open your Notification Settings and select Xcodes if you wish to allow access.")
Text("AccessDenied")
.fixedSize(horizontal: false, vertical: true)
Button("Notification Settings", action: {
Button("NotificationSettings", action: {
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.notifications")!)
})
default:
Button("Enable Notifications", action: {
Button("EnableNotifications", action: {
Current.notificationManager.requestAccess()
})
}

View file

@ -12,12 +12,12 @@ struct UpdatesPreferencePane: View {
GroupBox(label: Text("Versions")) {
VStack(alignment: .leading) {
Toggle(
"Automatically install new versions of Xcode",
"AutomaticInstallNewVersion",
isOn: $autoInstallationType.isAutoInstalling
)
Toggle(
"Include prerelease/beta versions",
"IncludePreRelease",
isOn: $autoInstallationType.isAutoInstallingBeta
)
}
@ -27,23 +27,23 @@ struct UpdatesPreferencePane: View {
Divider()
GroupBox(label: Text("Xcodes.app Updates")) {
GroupBox(label: Text("AppUpdates")) {
VStack(alignment: .leading) {
Toggle(
"Automatically check for app updates",
"CheckForAppUpdates",
isOn: $updater.automaticallyChecksForUpdates
)
Toggle(
"Include prerelease app versions",
"IncludePreRelease",
isOn: $updater.includePrereleaseVersions
)
Button("Check Now") {
Button("CheckNow") {
SUUpdater.shared()?.checkForUpdates(nil)
}
Text("Last checked: \(lastUpdatedString)")
Text(String(format: localizeString("LastChecked"), lastUpdatedString))
.font(.footnote)
}
.frame(maxWidth: .infinity, alignment: .leading)
@ -57,7 +57,7 @@ struct UpdatesPreferencePane: View {
if let lastUpdatedDate = updater.lastUpdateCheckDate {
return Self.formatter.string(from: lastUpdatedDate)
} else {
return "Never"
return localizeString("Never")
}
}

View file

@ -10,7 +10,7 @@ struct SignIn2FAView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Enter the \(authOptions.securityCode.length) digit code from one of your trusted devices:")
Text(String(format: localizeString("DigitCodeDescription"), authOptions.securityCode.length))
HStack {
Spacer()
@ -22,7 +22,7 @@ struct SignIn2FAView: View {
HStack {
Button("Cancel", action: { isPresented = false })
.keyboardShortcut(.cancelAction)
Button("Send SMS", action: { appState.choosePhoneNumberForSMS(authOptions: authOptions, sessionData: sessionData) })
Button("SendSMS", action: { appState.choosePhoneNumberForSMS(authOptions: authOptions, sessionData: sessionData) })
Spacer()
ProgressButton(isInProgress: appState.isProcessingAuthRequest,
action: { appState.submitSecurityCode(.device(code: code), sessionData: sessionData) }) {

View file

@ -7,16 +7,16 @@ struct SignInCredentialsView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Sign in with your Apple ID.")
Text("SignInWithApple")
.bold()
.padding(.vertical)
HStack {
Text("Apple ID:")
Text("AppleID")
.frame(minWidth: 100, alignment: .trailing)
TextField("example@icloud.com", text: $username)
}
HStack {
Text("Password:")
Text("Password")
.frame(minWidth: 100, alignment: .trailing)
SecureField("Required", text: $password)
}

View file

@ -11,14 +11,14 @@ struct SignInPhoneListView: View {
var body: some View {
VStack(alignment: .leading) {
if let phoneNumbers = authOptions.trustedPhoneNumbers, !phoneNumbers.isEmpty {
Text("Select a trusted phone number to receive a \(authOptions.securityCode.length) digit code via SMS:")
Text(String(format: localizeString("SelectTrustedPhone"), authOptions.securityCode.length))
List(phoneNumbers, selection: $selectedPhoneNumberID) {
Text($0.numberWithDialCode)
}
} else {
AttributedText(
NSAttributedString(string: "Your account doesn't have any trusted phone numbers, but they're required for two-factor authentication.\n\nSee https://support.apple.com/en-ca/HT204915.")
NSAttributedString(string: localizeString("NoTrustedPhones"))
.convertingURLsToLinkAttributes()
)
Spacer()

View file

@ -11,7 +11,7 @@ struct SignInSMSView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Enter the \(authOptions.securityCode.length) digit code sent to \(trustedPhoneNumber.numberWithDialCode): ")
Text(String(format: localizeString("EnterDigitCodeDescription"), authOptions.securityCode.length, trustedPhoneNumber.numberWithDialCode))
HStack {
Spacer()

View file

@ -10,7 +10,7 @@ struct SignedInView: View {
var body: some View {
HStack(alignment:.top, spacing: 10) {
Text(username)
Button("Sign Out", action: appState.signOut)
Button("SignOut", action: appState.signOut)
}
.frame(maxWidth: .infinity, alignment: .leading)
}

View file

@ -22,7 +22,7 @@ struct InstallationStepRowView: View {
.scaleEffect(0.5)
}
Text("Step \(installationStep.stepNumber) of \(installationStep.stepCount): \(installationStep.message)")
Text(String(format: localizeString("InstallationStepDescription"), installationStep.stepNumber, installationStep.stepCount, installationStep.message))
.font(.footnote)
Button(action: cancel) {
@ -31,7 +31,7 @@ struct InstallationStepRowView: View {
}
.buttonStyle(PlainButtonStyle())
.foregroundColor(highlighted ? .white : .secondary)
.help("Stop installation")
.help("StopInstallation")
}
.frame(minWidth: 80)
}

View file

@ -17,7 +17,7 @@ struct MainToolbarModifier: ViewModifier {
Button(action: { appState.presentedSheet = .signIn }, label: {
Label("Login", systemImage: "person.circle")
})
.help("Open Login")
.help("LoginDescription")
ProgressButton(
isInProgress: appState.isUpdating,
@ -26,7 +26,7 @@ struct MainToolbarModifier: ViewModifier {
Label("Refresh", systemImage: "arrow.clockwise")
}
.keyboardShortcut(KeyEquivalent("r"))
.help("Refresh Xcode List")
.help("RefreshDescription")
Button(action: {
switch category {
@ -40,27 +40,27 @@ struct MainToolbarModifier: ViewModifier {
Label("All", systemImage: "line.horizontal.3.decrease.circle")
case .release:
if #available(macOS 11.3, *) {
Label("Release only", systemImage: "line.horizontal.3.decrease.circle.fill")
Label("ReleaseOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
.labelStyle(TitleAndIconLabelStyle())
.foregroundColor(.accentColor)
} else {
Label("Release only", systemImage: "line.horizontal.3.decrease.circle.fill")
Label("ReleaseOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
.labelStyle(TitleOnlyLabelStyle())
.foregroundColor(.accentColor)
}
case .beta:
if #available(macOS 11.3, *) {
Label("Beta only", systemImage: "line.horizontal.3.decrease.circle.fill")
Label("BetaOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
.labelStyle(TitleAndIconLabelStyle())
.foregroundColor(.accentColor)
} else {
Label("Beta only", systemImage: "line.horizontal.3.decrease.circle.fill")
Label("BetaOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
.labelStyle(TitleOnlyLabelStyle())
.foregroundColor(.accentColor)
}
}
}
.help("Filter available versions")
.help("FilterAvailableDescription")
Button(action: {
isInstalledOnly.toggle()
@ -73,7 +73,7 @@ struct MainToolbarModifier: ViewModifier {
}
}
.help("Filter installed versions")
.help("FilterInstalledDescription")
Button(action: { isShowingInfoPane.toggle() }) {
if isShowingInfoPane {
@ -84,17 +84,17 @@ struct MainToolbarModifier: ViewModifier {
}
}
.keyboardShortcut(KeyboardShortcut("i", modifiers: [.command, .option]))
.help("Show or hide the info pane")
.help("InfoDescription")
Button(action: { NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) }, label: {
Label("Preferences", systemImage: "gearshape")
})
.help("Open Preferences")
.help("PreferencesDescription")
TextField("Search...", text: $searchText)
TextField("Search", text: $searchText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 200)
.help("Search list")
.help("SearchDescription")
}
}
}

View file

@ -9,9 +9,9 @@ enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConverti
var description: String {
switch self {
case .all: return "All"
case .release: return "Release"
case .beta: return "Beta"
case .all: return localizeString("All")
case .release: return localizeString("Release")
case .beta: return localizeString("Beta")
}
}
}

View file

@ -20,9 +20,9 @@ struct XcodeListViewRow: View {
Image(systemName: "square.fill.on.square.fill")
.font(.subheadline)
.foregroundColor(.secondary)
.accessibility(label: Text("Identical Builds"))
.accessibility(label: Text("IdenticalBuilds"))
.accessibility(value: Text(xcode.identicalBuilds.map(\.appleDescription).joined(separator: ", ")))
.help("Sometimes a prerelease and release version are the exact same build. Xcodes will automatically display these versions together.")
.help("IdenticalBuilds.help")
}
}
@ -84,14 +84,14 @@ struct XcodeListViewRow: View {
if xcode.selected {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
.help("This is the active version")
.help("ActiveVersionDescription")
} else {
Button(action: { appState.select(xcode: xcode) }) {
Image(systemName: "checkmark.circle")
.foregroundColor(.secondary)
}
.buttonStyle(PlainButtonStyle())
.help("Make this the active version")
.help("MakeActiveVersionDescription")
}
} else {
EmptyView()
@ -102,13 +102,15 @@ struct XcodeListViewRow: View {
private func installControl(for xcode: Xcode) -> some View {
switch xcode.installState {
case .installed:
Button("OPEN") { appState.open(xcode: xcode) }
Button("Open") { appState.open(xcode: xcode) }
.textCase(.uppercase)
.buttonStyle(AppStoreButtonStyle(primary: true, highlighted: selected))
.help("Open this version")
.help("OpenDescription")
case .notInstalled:
Button("INSTALL") { appState.checkMinVersionAndInstall(id: xcode.id) }
Button("Install") { appState.checkMinVersionAndInstall(id: xcode.id) }
.textCase(.uppercase)
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: selected))
.help("Install this version")
.help("InstallDescription")
case let .installing(installationStep):
InstallationStepRowView(
installationStep: installationStep,

View file

@ -0,0 +1,215 @@
// Menu
"Menu.About" = "About Xcodes";
"Menu.CheckForUpdates" = "Check for Updates...";
"Menu.Acknowledgements" = "Xcodes Acknowledgements";
"Menu.GitHubRepo" = "Xcodes GitHub Repo";
"Menu.ReportABug" = "Report a Bug";
"Menu.RequestNewFeature" = "Request a New Feature";
// Common
"Install" = "Install";
"InstallDescription" = "Install this version";
"RevealInFinder" = "Reveal in Finder";
"Active" = "Active";
"MakeActive" = "Make active";
"Open" = "Open";
"OpenDescription" = "Open this version";
"CopyPath" = "Copy Path";
"CreateSymLink" = "Create Symlink as Xcode.app";
"Uninstall" = "Uninstall";
"Selected" = "Selected";
"Select" = "Select";
"Cancel" = "Cancel";
"Next" = "Next";
"Continue" = "Continue";
"Close" = "Close";
// Info Pane
"IdenticalBuilds" = "Identical Builds";
"IdenticalBuilds.help" = "Sometimes a prerelease and release version are the exact same build. Xcodes will automatically display these versions together.";
"ReleaseDate" = "Release Date";
"ReleaseNotes" = "Release Notes";
"ReleaseNotes.help" = "View Release Notes";
"Compatibility" = "Compatibility";
"MacOSRequirement" = "Requires macOS %@ or later";
"SDKs" = "SDKs";
"Compilers" = "Compilers";
"DownloadSize" = "Download Size";
"NoXcodeSelected" = "No Xcode Selected";
// Installation Steps
"InstallationStepDescription" = "Step %@ of %@: %@";
"DownloadingPercentDescription" = "Downloading: %@% complete";
"StopInstallation" = "Stop installation";
"DownloadingError" = "No download information found";
// About
"VersionWithBuild" = "Version %@ (%@)";
"GithubRepo" = "GitHub Repo";
"Acknowledgements" = "Acknowledgements";
"UnxipExperiment" = "Unxip Experiment";
"License" = "License";
// General Preference Pane
"General" = "General";
"AppleID" = "Apple ID";
"SignIn" = "Sign In";
"Notifications" = "Notifications";
// Updates Preference Pane
"Updates" = "Updates";
"Versions" = "Versions";
"AutomaticInstallNewVersion" = "Automatically install new versions of Xcode";
"IncludePreRelease" = "Include prerelease/beta versions";
"AppUpdates" = "Xcodes.app Updates";
"CheckForAppUpdates" = "Automatically check for app updates";
"CheckNow" = "Check Now";
"LastChecked" = "Last checked: %@";
"Never" = "Never";
// Advanced Preference Pane
"Advanced" = "Advanced";
"LocalCachePath" = "Local Cache Path";
"LocalCachePathDescription" = "Xcodes caches available Xcode versions and temporary downloads new versions to a directory";
"Change" = "Change";
"Active/Select" = "Active/Select";
"AutomaticallyCreateSymbolicLink" = "Automatically create symbolic link to Xcodes.app";
"AutomaticallyCreateSymbolicLinkDescription" = "When making an Xcode version Active/Selected, try and create a symbolic link named Xcode.app in the installation directory";
"DataSource" = "Data Source";
"DataSourceDescription" = "The Apple data source scrapes the Apple Developer website. It will always show the latest releases that are available, but is more fragile.\n\nXcode Releases is an unofficial list of Xcode releases. It's provided as well-formed data, contains extra information that is not readily available from Apple, and is less likely to break if Apple redesigns their developer website.";
"Downloader" = "Downloader";
"DownloaderDescription" = "aria2 uses up to 16 connections to download Xcode 3-5x faster than URLSession. It's bundled as an executable along with its source code within Xcodes to comply with its GPLv2 license.\n\nURLSession is the default Apple API for making URL requests.";
"PrivilegedHelper" = "Privileged Helper";
"PrivilegedHelperDescription" = "Xcodes uses a separate privileged helper to perform tasks as root. These are things that would require sudo on the command line, including post-install steps and switching Xcode versions with xcode-select.\n\nYou'll be prompted for your macOS account password to install it.";
"HelperInstalled" = "Helper is installed";
"HelperNotInstalled" = "Helper is not installed";
"InstallHelper" = "Install helper";
// Experiment Preference Pane
"Experiments" = "Experiments";
"FasterUnxip" = "Faster Unxip";
"UseUnxipExperiment" = "When unxipping, use experiment";
"FasterUnxipDescription" = "Thanks to @_saagarjha, this experiment can increase unxipping speed by up to 70% for some systems.\n\nMore information on how this is accomplished can be seen on the unxip repo - https://github.com/saagarjha/unxip";
// Notifications
"AccessGranted" = "Access Granted. You will receive notifications from Xcodes.";
"AccessDenied" = "⚠️ Access Denied ⚠️\n\nPlease open your Notification Settings and select Xcodes if you wish to allow access.";
"NotificationSettings" = "Notification Settings";
"EnableNotifications" = "Enable Notifications";
// SignIn
"SignInWithApple" = "Sign in with your Apple ID.";
"AppleID" = "AppleID:";
"Password" = "Password:";
"Required" = "Required";
"SignOut" = "Sign Out";
// SMS/2FA
"DigitCodeDescription" = "Enter the %@ digit code from one of your trusted devices:";
"SendSMS" = "Send SMS";
"EnterDigitCodeDescription" = "Enter the %@ digit code sent to %@: ";
"SelectTrustedPhone" = "Select a trusted phone number to receive a %@ digit code via SMS:";
"NoTrustedPhones" = "Your account doesn't have any trusted phone numbers, but they're required for two-factor authentication.\n\nSee https://support.apple.com/en-ca/HT204915.";
// MainWindow
"UpdatedAt" = "Updated at";
// ToolBar
"Login" = "Login";
"LoginDescription" = "Open Login";
"Refresh" = "Refresh";
"RefreshDescription" = "Refresh Xcode List";
"All" = "All";
"Release" = "Release";
"ReleaseOnly" = "Release only";
"Beta" = "Beta";
"BetaOnly" = "Beta only";
"Filter" = "Filter";
"FilterAvailableDescription" = "Filter available versions";
"FilterInstalledDescription" = "Filter installed versions";
"Info" = "Info";
"InfoDescription" = "Show or hide the info pane";
"Preferences" = "Preferences";
"PreferencesDescription" = "Open Preferences";
"Search" = "Search...";
"SearchDescription" = "Search list";
// List
"ActiveVersionDescription" = "This is the active version";
"MakeActiveVersionDescription" = "Make this the active version";
// Alerts
// Uninstall
"Alert.Uninstall.Title" = "Uninstall Xcode %@?";
"Alert.Uninstall.Message" = "It will be moved to the Trash, but won't be emptied.";
"Alert.Uninstall.Error.Title" = "Unable to uninstall Xcode";
// Cancel Install
"Alert.CancelInstall.Title" = "Are you sure you want to stop the installation of Xcode %@?";
"Alert.CancelInstall.Message" = "Any progress will be discarded.";
"Alert.CancelInstall.PrimaryButton" = "Stop Installation";
// Privileged Helper
"Alert.PrivilegedHelper.Title" = "Privileged Helper";
"Alert.PrivilegedHelper.Message" = "Xcodes uses a separate privileged helper to perform tasks as root. These are things that would require sudo on the command line, including post-install steps and switching Xcode versions with xcode-select.\n\nYou'll be prompted for your macOS account password to install it.";
"Alert.PrivilegedHelper.Error.Title" = "Unable to install helper";
// Min MacOS Supported
"Alert.MinSupported.Title" = "Minimum requirements not met";
"Alert.MinSupported.Message" = "Xcode %@ requires MacOS %@, but you are running MacOS %@, do you still want to install it?";
// Install
"Alert.Install.Error.Title" = "Unable to install Xcode";
"Alert.InstallArchive.Error.Title" = "Unable to install archived Xcode";
// Update
"Alert.Update.Error.Title" = "Unable to update selected Xcode";
// Active/Select
"Alert.Select.Error.Title" = "Unable to select Xcode";
// Symbolic Links
"Alert.SymLink.Title" = "Unable to create symbolic Link";
"Alert.SymLink.Message" = "Xcode.app exists and is not a symbolic link";
// Post install
"Alert.PostInstall.Title" = "Unable to perform post install steps";
// InstallationErrors
"InstallationError.DamagedXIP" = "The archive \"%@\" is damaged and can't be expanded.";
"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "The archive \"%@\" cant be expanded because the current volume doesnt have enough free space.\n\nMake more space available to expand the archive and then install Xcode %@ again to start installation from where it left off.";
"InstallationError.FailedToMoveXcodeToApplications" = "Failed to move Xcode to the %@ directory.";
"InstallationError.FailedSecurityAssessment" = "Xcode %@ failed its security assessment with the following output:\n%@\nIt remains installed at %@ if you wish to use it anyways.";
"InstallationError.CodesignVerifyFailed" = "The downloaded Xcode failed code signing verification with the following output:\n%@";
"InstallationError.UnexpectedCodeSigningIdentity" = "The downloaded Xcode doesn't have the expected code signing identity.\nGot:\n%@\n%@\nExpected:\n%@\n%@";
"InstallationError.UnsupportedFileFormat" = "Xcodes doesn't (yet) support installing Xcode from the %@ file format.";
"InstallationError.MissingSudoerPassword" = "Missing password. Please try again.";
"InstallationError.UnavailableVersion" = "Could not find version %@.";
"InstallationError.NoNonPrereleaseVersionAvailable" = "No non-prerelease versions available.";
"InstallationError.NoPrereleaseVersionAvailable" = "No prerelease versions available.";
"InstallationError.MissingUsernameOrPassword" = "Missing username or a password. Please try again.";
"InstallationError.VersionAlreadyInstalled" = "%@ is already installed at %@";
"InstallationError.InvalidVersion" = "%@ is not a valid version number.";
"InstallationError.VersionNotInstalled" = "%@ is not installed.";
"InstallationError.PostInstallStepsNotPerformed.Installed" = "Installation was completed, but some post-install steps weren't performed automatically. These will be performed when you first launch Xcode %@.";
"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "Installation was completed, but some post-install steps weren't performed automatically. Xcodes performs these steps with a privileged helper, which appears to not be installed. You can install it from Preferences > Advanced.\n\nThese steps will be performed when you first launch Xcode %@.";
// Installation Steps
"Downloading" = "Downloading";
"Unarchiving" = "Unarchiving (This can take a while)";
"Moving" = "Moving to %@";
"TrashingArchive" = "Moving archive to the Trash";
"CheckingSecurity" = "Security verification";
"Finishing" = "Finishing";
// Notifications
"Notification.NewVersionAvailable" = "New version is available";
"Notification.FinishedInstalling" = "Finished installing";
"HelperClient.error" = "Unable to communicate with privileged helper.";
///++
// Notifications
"Notification.NewXcodeVersion.Title" = "New Xcode versions";
"Notification.NewXcodeVersion.Body" = "New Xcode versions are available to download.";

View file

@ -26,12 +26,12 @@ struct XcodesApp: App {
}
.commands {
CommandGroup(replacing: .appInfo) {
Button("About Xcodes") {
Button("Menu.About") {
appDelegate.showAboutWindow()
}
}
CommandGroup(after: .appInfo) {
Button("Check for Updates...") {
Button("Menu.CheckForUpdates") {
appDelegate.checkForUpdates()
}
}
@ -47,19 +47,19 @@ struct XcodesApp: App {
XcodeCommands(appState: appState)
CommandGroup(replacing: CommandGroupPlacement.help) {
Button("Xcodes GitHub Repo") {
Button("Menu.GitHubRepo") {
let xcodesRepoURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/")!
openURL(xcodesRepoURL)
}
Divider()
Button("Report a Bug") {
Button("Menu.ReportABug") {
let bugReportURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/issues/new?assignees=&labels=bug&template=bug_report.md&title=")!
openURL(bugReportURL)
}
Button("Request a New Feature") {
Button("Menu.RequestNewFeature") {
let featureRequestURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=")!
openURL(featureRequestURL)
}
@ -81,7 +81,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
backing: .buffered,
defer: false
)) {
$0.title = "About Xcodes"
$0.title = localizeString("About")
$0.contentView = NSHostingView(rootView: AboutView(showAcknowledgementsWindow: showAcknowledgementsWindow))
$0.isReleasedWhenClosed = false
}
@ -92,7 +92,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
backing: .buffered,
defer: false
)) {
$0.title = "Xcodes Acknowledgements"
$0.title = localizeString("Acknowledgements")
$0.contentView = NSHostingView(rootView: AcknowledgmentsView())
$0.isReleasedWhenClosed = false
}
@ -121,3 +121,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
SUUpdater.shared()
}
}
func localizeString(_ key: String, comment: String = "") -> String {
if #available(macOS 12, *) {
return String(localized: String.LocalizationValue(key))
} else {
return NSLocalizedString(key, comment: comment)
}
}