From 799166f8abe0cf7594b589432e50fdc74005e797 Mon Sep 17 00:00:00 2001 From: ezn1hero Date: Wed, 13 May 2026 11:12:10 -0700 Subject: [PATCH] Add Russian and English localization --- MuffinStoreJailed/App/LogView.swift | 2 +- MuffinStoreJailed/App/NavigationButtons.swift | 19 +-- MuffinStoreJailed/App/SettingsView.swift | 60 +++++++--- MuffinStoreJailed/AppData.swift | 21 +++- MuffinStoreJailed/ContentView.swift | 46 +++---- MuffinStoreJailed/Functions/Downgrader.swift | 88 ++++++++------ MuffinStoreJailed/Functions/IPATool.swift | 69 ++++++----- .../Localization/Localization.swift | 52 ++++++++ MuffinStoreJailed/MuffinStoreJailedApp.swift | 8 ++ .../en.lproj/Localizable.strings | 112 ++++++++++++++++++ .../ru.lproj/Localizable.strings | 112 ++++++++++++++++++ PancakeStore.xcodeproj/project.pbxproj | 17 +-- .../xcschemes/PancakeStore.xcscheme | 2 +- 13 files changed, 485 insertions(+), 123 deletions(-) create mode 100644 MuffinStoreJailed/Localization/Localization.swift create mode 100644 MuffinStoreJailed/en.lproj/Localizable.strings create mode 100644 MuffinStoreJailed/ru.lproj/Localizable.strings diff --git a/MuffinStoreJailed/App/LogView.swift b/MuffinStoreJailed/App/LogView.swift index a1f1e1c..4b9f2f2 100644 --- a/MuffinStoreJailed/App/LogView.swift +++ b/MuffinStoreJailed/App/LogView.swift @@ -36,7 +36,7 @@ struct LogView: View { Button { UIPasteboard.general.string = log } label: { - Label("Copy Output", systemImage: "doc.on.doc") + Label(Localization.string("action.copyOutput"), systemImage: "doc.on.doc") } } } diff --git a/MuffinStoreJailed/App/NavigationButtons.swift b/MuffinStoreJailed/App/NavigationButtons.swift index 468eed3..7efae6c 100644 --- a/MuffinStoreJailed/App/NavigationButtons.swift +++ b/MuffinStoreJailed/App/NavigationButtons.swift @@ -19,7 +19,10 @@ struct NavigationButtons: View { Haptic.shared.play(.soft) DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { if appData.appleId.isEmpty || appData.password.isEmpty { - Alertinator.shared.alert(title: "No Apple ID details were input!", body: "Please type both your Apple ID email address & password, then try again.") + Alertinator.shared.alert( + title: Localization.string("alert.login.missingCredentials.title"), + body: Localization.string("alert.login.missingCredentials.message") + ) } else { if appData.code.isEmpty { appData.ipaTool = IPATool(appleId: appData.appleId, password: appData.password) @@ -33,7 +36,7 @@ struct NavigationButtons: View { appData.isAuthenticated = ret ?? false if appData.isAuthenticated { - appData.applicationStatus = "Ready to Downgrade!" + appData.setStatus(.readyToDowngrade) appData.applicationIcon = "checkmark.circle.fill" appData.applicationIconColor = .secondary } @@ -41,9 +44,9 @@ struct NavigationButtons: View { } }) { if appData.hasSent2FACode { - ButtonLabel(text: "Log In", icon: "arrow.right") + ButtonLabel(text: Localization.string("action.logIn"), icon: "arrow.right") } else { - ButtonLabel(text: "Send 2FA Code", icon: "key") + ButtonLabel(text: Localization.string("action.sendTwoFactor"), icon: "key") } } .buttonStyle(FancyButtonStyle()) @@ -57,7 +60,7 @@ struct NavigationButtons: View { LSApplicationWorkspace.default().openApplication(withBundleID: "com.jbdotparty.PancakeStore2") } }) { - ButtonLabel(text: "Open App", icon: "arrow.up.forward.app") + ButtonLabel(text: Localization.string("action.openApp"), icon: "arrow.up.forward.app") } .buttonStyle(FancyButtonStyle(color: .blue)) .disabled(!appData.hasAppBeenServed) @@ -68,7 +71,7 @@ struct NavigationButtons: View { exitinator() } }) { - ButtonLabel(text: "Go to Home Screen", icon: "house") + ButtonLabel(text: Localization.string("action.goHome"), icon: "house") } .buttonStyle(FancyButtonStyle()) .disabled(!appData.hasAppBeenServed) @@ -90,11 +93,11 @@ struct NavigationButtons: View { print("App ID: \(appLinkParsed)") appData.isDowngrading = true downgradeApp(appId: appLinkParsed, ipaTool: appData.ipaTool!) - appData.applicationStatus = "Downgrading Application..." + appData.setStatus(.downgrading) appData.applicationIcon = "showMeProgressPlease" } }) { - ButtonLabel(text: "Downgrade App", icon: "arrow.down") + ButtonLabel(text: Localization.string("action.downgradeApp"), icon: "arrow.down") } .buttonStyle(FancyButtonStyle()) .disabled(appData.appLink.isEmpty) diff --git a/MuffinStoreJailed/App/SettingsView.swift b/MuffinStoreJailed/App/SettingsView.swift index 2b0519a..c86fd0e 100644 --- a/MuffinStoreJailed/App/SettingsView.swift +++ b/MuffinStoreJailed/App/SettingsView.swift @@ -14,69 +14,90 @@ struct SettingsView: View { @Environment(\.openURL) private var openURL @AppStorage("autoCleanApp") var autoCleanApp: Bool = true + @AppStorage(AppLanguage.storageKey) private var languageCode: String = AppLanguage.english.rawValue var body: some View { NavigationStack { List { - Section(header: HeaderLabel(text: "About", icon: "info.circle")) { + Section(header: HeaderLabel(text: Localization.string("section.about.title"), icon: "info.circle")) { VStack(alignment: .leading, spacing: 10) { AppInfoCell() HStack { Button(action: { openURL(URL(string: "https://jailbreak.party/discord")!) }) { - ButtonLabel(text: "Discord", icon: "discord", useImage: true) + ButtonLabel(text: Localization.string("action.discord"), icon: "discord", useImage: true) } .buttonStyle(TranslucentButtonStyle(color: .discord)) Button(action: { openURL(URL(string: "https://jailbreak.party/discord")!) }) { - ButtonLabel(text: "GitHub", icon: "github", useImage: true) + ButtonLabel(text: Localization.string("action.github"), icon: "github", useImage: true) } .buttonStyle(TranslucentButtonStyle(color: .github)) } Button(action: { openURL(URL(string: "https://jailbreak.party/discord")!) }) { - ButtonLabel(text: "Website", icon: "globe") + ButtonLabel(text: Localization.string("action.website"), icon: "globe") } .buttonStyle(TranslucentButtonStyle()) } } - Section(header: HeaderLabel(text: "Settings", icon: "gearshape")) { + Section(header: HeaderLabel(text: Localization.string("section.settings.title"), icon: "gearshape")) { Toggle(isOn: $autoCleanApp) { - Text("Auto-Clean App") - Text("This is toggled on by default to make sure that PancakeStore doesn't keep any saved data from the app you had downgraded.") + Text("settings.autoClean.title") + Text("settings.autoClean.subtitle") + } + } + + Section(header: HeaderLabel(text: Localization.string("section.language.title"), icon: "globe")) { + VStack(spacing: 12) { + ForEach(AppLanguage.allCases) { (language: AppLanguage) in + Button(action: { + select(language) + }) { + HStack { + Text(language.displayNameKey) + Spacer() + if language.rawValue == languageCode { + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(.tint) + } + } + } + .buttonStyle(TranslucentButtonStyle()) + } } } - Section(header: HeaderLabel(text: "Data", icon: "loupe"), footer: Text("If PancakeStore is taking up a lot of storage, click this button.")) { + Section(header: HeaderLabel(text: Localization.string("section.data.title"), icon: "loupe"), footer: Text("section.data.footer")) { VStack { Button(action: { let tempDir = FileManager.default.temporaryDirectory let tempIPAURL = tempDir.appendingPathComponent("app.ipa") presentShareSheet(with: tempIPAURL) }) { - ButtonLabel(text: "Export IPA", icon: "arrow.up.doc") + ButtonLabel(text: Localization.string("action.exportIpa"), icon: "arrow.up.doc") } .buttonStyle(TranslucentButtonStyle()) .disabled(!appData.hasAppBeenServed) Button(action: { cleanUp() }) { - ButtonLabel(text: "Clean Documents", icon: "trash") + ButtonLabel(text: Localization.string("action.cleanDocuments"), icon: "trash") } .buttonStyle(TranslucentButtonStyle()) } } - Section(header: HeaderLabel(text: "Credits", icon: "star")) { - LinkCreditCell(image: Image("mineek"), name: "mineek", description: "Original Project, MuffinStore Jailed.", url: "https://github.com/mineek") - LinkCreditCell(image: Image("lunginspector"), name: "lunginspector", description: "UI changes and QoL improvements.", url: "https://github.com/lunginspector") - LinkCreditCell(image: Image("skadz"), name: "Skadz", description: "Backend fixes.", url: "https://github.com/skadz108") + Section(header: HeaderLabel(text: Localization.string("section.credits.title"), icon: "star")) { + LinkCreditCell(image: Image("mineek"), name: "mineek", description: Localization.string("credits.mineek"), url: "https://github.com/mineek") + LinkCreditCell(image: Image("lunginspector"), name: "lunginspector", description: Localization.string("credits.lunginspector"), url: "https://github.com/lunginspector") + LinkCreditCell(image: Image("skadz"), name: "Skadz", description: Localization.string("credits.skadz"), url: "https://github.com/skadz108") } } - .navigationTitle("Settings") + .navigationTitle("settings.title") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button(action: { @@ -89,6 +110,15 @@ struct SettingsView: View { } } } + +private extension SettingsView { + func select(_ language: AppLanguage) { + Haptic.shared.play(.soft) + languageCode = language.rawValue + Localization.setLanguage(language) + appData.refreshLocalization() + } +} /* struct SettingsView: View { @Environment(\.dismiss) private var dismiss diff --git a/MuffinStoreJailed/AppData.swift b/MuffinStoreJailed/AppData.swift index 0fbedab..04fc742 100644 --- a/MuffinStoreJailed/AppData.swift +++ b/MuffinStoreJailed/AppData.swift @@ -7,12 +7,22 @@ import SwiftUI +enum ApplicationStatusKey: String { + case notLoggedIn = "status.notLoggedIn" + case readyToDowngrade = "status.readyToDowngrade" + case downgrading = "status.downgrading" + case downgradeSuccessful = "status.downgradeSuccessful" + + var localizationKey: String { rawValue } +} + final class AppData: ObservableObject { static let shared = AppData() @Published var applicationIcon: String = "xmark.circle.fill" @Published var applicationIconColor: Color = .secondary - @Published var applicationStatus: String = "Not logged in!" + @Published private(set) var applicationStatus: String = Localization.string(ApplicationStatusKey.notLoggedIn.localizationKey) + private var applicationStatusKey: ApplicationStatusKey = .notLoggedIn @Published var appBundleID: String = "" @Published var appVersion: String = "" @@ -33,5 +43,14 @@ final class AppData: ObservableObject { @Published var hasSent2FACode: Bool = false @Published var showPassword: Bool = false + + func setStatus(_ status: ApplicationStatusKey) { + applicationStatusKey = status + applicationStatus = Localization.string(status.localizationKey) + } + + func refreshLocalization() { + applicationStatus = Localization.string(applicationStatusKey.localizationKey) + } } diff --git a/MuffinStoreJailed/ContentView.swift b/MuffinStoreJailed/ContentView.swift index 737f66f..5c73ac6 100644 --- a/MuffinStoreJailed/ContentView.swift +++ b/MuffinStoreJailed/ContentView.swift @@ -24,7 +24,7 @@ struct ContentView: View { LogsSection NavigationButtons() } - .navigationTitle("PancakeStore") + .navigationTitle("app.name") }) { List { if !appData.isAuthenticated { @@ -52,7 +52,7 @@ struct ContentView: View { } } } - .navigationTitle("PancakeStore") + .navigationTitle("app.name") .toolbar { ToolbarItem(placement: .topBarTrailing) { Menu { @@ -61,7 +61,7 @@ struct ContentView: View { let tempIPAURL = tempDir.appendingPathComponent("app.ipa") presentShareSheet(with: tempIPAURL) }) { - Label("Export IPA", systemImage: "arrow.up.doc") + Label(Localization.string("action.exportIpa"), systemImage: "arrow.up.doc") } .disabled(!appData.hasAppBeenServed) Button(action: { @@ -73,7 +73,7 @@ struct ContentView: View { exitinator() } }) { - ButtonLabel(text: "Log Out", icon: "arrow.right") + ButtonLabel(text: Localization.string("action.logOut"), icon: "arrow.right") } .disabled(!appData.isAuthenticated) } label : { Image(systemName: "line.horizontal.3") } @@ -98,13 +98,13 @@ struct ContentView: View { } .onAppear { appData.isAuthenticated = EncryptedKeychainWrapper.hasAuthInfo() - print("Found \(appData.isAuthenticated ? "auth" : "no auth") info in keychain") + print(Localization.string(appData.isAuthenticated ? "log.keychain.foundAuth" : "log.keychain.foundNoAuth")) if appData.isAuthenticated { - appData.applicationStatus = "Ready to Downgrade!" + appData.setStatus(.readyToDowngrade) appData.applicationIcon = "checkmark.circle.fill" appData.applicationIconColor = .primary guard let authInfo = EncryptedKeychainWrapper.getAuthInfo() else { - print("Failed to get auth info from keychain, logging out") + print(Localization.string("log.keychain.failedGet")) appData.isAuthenticated = false EncryptedKeychainWrapper.nuke() EncryptedKeychainWrapper.generateAndStoreKey() @@ -114,16 +114,16 @@ struct ContentView: View { appData.password = authInfo["password"]! as! String appData.ipaTool = IPATool(appleId: appData.appleId, password: appData.password) let ret = appData.ipaTool?.authenticate() - print("Re-authenticated \(ret! ? "successfully" : "unsuccessfully")") + print(Localization.string(ret! ? "log.reauth.success" : "log.reauth.failure")) } else { - print("No auth info found in keychain, setting up by generating a key in SEP") + print(Localization.string("log.keychain.noAuthGenerating")) EncryptedKeychainWrapper.generateAndStoreKey() } } } private var LogsSection: some View { - Section(header: HeaderLabel(text: "Logs", icon: "terminal"), footer: Text("Originally created by [mineek](https://github.com/mineek) with backend and QoL enhancements made by [jailbreak.party](https://github.com/jailbreakdotparty).\n[Join the jailbreak.party discord!](https://jailbreak.party/discord)")) { + Section(header: HeaderLabel(text: Localization.string("section.logs.title"), icon: "terminal"), footer: Text("section.logs.footer")) { VStack { TerminalHeader(text: appData.applicationStatus, icon: appData.applicationIcon, color: appData.applicationIconColor) LogView() @@ -134,9 +134,9 @@ struct ContentView: View { private var LoginSection: some View { Group { - Section(header: HeaderLabel(text: "Login", icon: "icloud"), footer: Text("")) { + Section(header: HeaderLabel(text: Localization.string("section.login.title"), icon: "icloud"), footer: Text("")) { VStack { - TextField("Apple ID", text: $appData.appleId) + TextField(Localization.string("login.placeholder.appleId"), text: $appData.appleId) .modifier(TextFieldBackground()) .disabled(appData.hasSent2FACode) .autocorrectionDisabled() @@ -144,13 +144,13 @@ struct ContentView: View { HStack { if appData.showPassword { - TextField("Password", text: $appData.password) + TextField(Localization.string("login.placeholder.password"), text: $appData.password) .modifier(TextFieldBackground()) .disabled(appData.hasSent2FACode) .autocorrectionDisabled() .textInputAutocapitalization(.never) } else { - SecureField("Password", text: $appData.password) + SecureField(Localization.string("login.placeholder.password"), text: $appData.password) .modifier(TextFieldBackground()) .disabled(appData.hasSent2FACode) .autocorrectionDisabled() @@ -169,8 +169,8 @@ struct ContentView: View { } if appData.hasSent2FACode { - Section(header: HeaderLabel(text: "Verification Code", icon: "key")) { - TextField("2FA Code", text: $appData.code) + Section(header: HeaderLabel(text: Localization.string("section.verification.title"), icon: "key")) { + TextField(Localization.string("login.placeholder.twoFactor"), text: $appData.code) .modifier(TextFieldBackground()) .keyboardType(.numberPad) } @@ -179,9 +179,9 @@ struct ContentView: View { } private var InputAppSection: some View { - Section(header: HeaderLabel(text: "Downgrade App", icon: "arrow.down.app"), footer: Text("To downgrade an app, it must have been purchased on your account at some point in the past (when the app has a cloud icon next to it). It must also not be installed on your device currently, but you can offload it.")) { + Section(header: HeaderLabel(text: Localization.string("section.downgrade.title"), icon: "arrow.down.app"), footer: Text("section.downgrade.footer")) { VStack { - TextField("Link to App Store App", text: $appData.appLink) + TextField(Localization.string("downgrade.placeholder.link"), text: $appData.appLink) .modifier(TextFieldBackground()) .autocorrectionDisabled() .textInputAutocapitalization(.never) @@ -190,10 +190,10 @@ struct ContentView: View { } private var AppInfoSection: some View { - Section(header: HeaderLabel(text: "App Info", icon: "info.circle")) { - ItemInfoCell(label: "App Link", icon: "link", text: appData.appLink) - ItemInfoCell(label: "App Bundle ID", icon: "shippingbox", text: appData.appBundleID) - ItemInfoCell(label: "Target App Version", icon: "arrow.down.app", text: appData.appVersion) + Section(header: HeaderLabel(text: Localization.string("section.appInfo.title"), icon: "info.circle")) { + ItemInfoCell(label: Localization.string("info.label.appLink"), icon: "link", text: appData.appLink) + ItemInfoCell(label: Localization.string("info.label.bundleId"), icon: "shippingbox", text: appData.appBundleID) + ItemInfoCell(label: Localization.string("info.label.targetVersion"), icon: "arrow.down.app", text: appData.appVersion) } } } @@ -221,7 +221,7 @@ struct ItemInfoCell: View { Button(action: { UIPasteboard.general.string = text }) { - Label("Copy Value", systemImage: "character.cursor.ibeam") + Label(Localization.string("action.copyValue"), systemImage: "character.cursor.ibeam") } } } diff --git a/MuffinStoreJailed/Functions/Downgrader.swift b/MuffinStoreJailed/Functions/Downgrader.swift index d13aff0..d795871 100644 --- a/MuffinStoreJailed/Functions/Downgrader.swift +++ b/MuffinStoreJailed/Functions/Downgrader.swift @@ -28,7 +28,7 @@ func downgradeAppToVersion(appId: String, versionId: String, ipaTool: IPATool) { @ObservedObject var appData = AppData.shared let path = ipaTool.downloadIPAForVersion(appId: appId, appVerId: versionId) - print("IPA downloaded to \(path)") + print(Localization.string("log.ipa.downloadedTo", arguments: path)) let tempDir = FileManager.default.temporaryDirectory var contents = try! FileManager.default.contentsOfDirectory(atPath: path) @@ -36,7 +36,7 @@ func downgradeAppToVersion(appId: String, versionId: String, ipaTool: IPATool) { // also delete this; i wanna see both the app's directory and the temp ipa GONE. let destinationUrl = tempDir.appendingPathComponent("app.ipa") try! Zip.zipFiles(paths: contents.map { URL(fileURLWithPath: path).appendingPathComponent($0) }, zipFilePath: destinationUrl, password: nil, progress: nil) - print("IPA zipped to \(destinationUrl)") + print(Localization.string("log.ipa.zippedTo", arguments: destinationUrl.path)) let path2 = URL(fileURLWithPath: path) var appDir = path2.appendingPathComponent("Payload") for file in try! FileManager.default.contentsOfDirectory(atPath: appDir.path) { @@ -64,15 +64,15 @@ func downgradeAppToVersion(appId: String, versionId: String, ipaTool: IPATool) { let server = Server() server.route(.GET, "signed.ipa", { _ in - print("Serving signed.ipa") + print(Localization.string("log.server.servingIpa")) let signedIPAData = try Data(contentsOf: destinationUrl) return HTTPResponse(body: signedIPAData) }) server.route(.GET, "install", { _ in - print("Serving install page") + print(Localization.string("log.server.servingInstall")) appData.hasAppBeenServed = true - appData.applicationStatus = "Downgrade successful!" + appData.setStatus(.downgradeSuccessful) appData.applicationIcon = "checkmark.circle.fill" appData.applicationIconColor = .green let installPage = """ @@ -84,10 +84,10 @@ func downgradeAppToVersion(appId: String, versionId: String, ipaTool: IPATool) { }) try! server.start(port: 9090) - print("Server has started listening") + print(Localization.string("log.server.started")) DispatchQueue.main.async { - print("Requesting app install") + print(Localization.string("log.server.requestingInstall")) // having it built-in no matter the version sounds more enjoyable, if you're already taking all the damn effort to do this bullshit then why not have this pop up on 17.x too? let safariView = SafariWebView(url: URL(string: "http://127.0.0.1:9090/install")!) @@ -108,25 +108,29 @@ func downgradeAppToVersion(appId: String, versionId: String, ipaTool: IPATool) { while server.isRunning { sleep(1) } - print("Server has stopped") + print(Localization.string("log.server.stopped")) } } func promptForVersionId(appId: String, versionIds: [String], ipaTool: IPATool) { let isiPad = UIDevice.current.userInterfaceIdiom == .pad - let alert = UIAlertController(title: "Enter version ID", message: "Select a version to downgrade to", preferredStyle: isiPad ? .alert : .actionSheet) + let alert = UIAlertController( + title: Localization.string("alert.version.enter.title"), + message: Localization.string("alert.version.enter.message"), + preferredStyle: isiPad ? .alert : .actionSheet + ) for versionId in versionIds { alert.addAction(UIAlertAction(title: versionId, style: .default, handler: { _ in downgradeAppToVersion(appId: appId, versionId: versionId, ipaTool: ipaTool) })) } - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + alert.addAction(UIAlertAction(title: Localization.string("action.cancel"), style: .cancel, handler: nil)) UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true, completion: nil) } func showAlert(title: String, message: String) { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: Localization.string("action.ok"), style: .default, handler: nil)) UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true, completion: nil) } @@ -137,7 +141,7 @@ func getAllAppVersionIdsFromServer(appId: String, ipaTool: IPATool) { let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { DispatchQueue.main.async { - showAlert(title: "Error", message: error.localizedDescription) + showAlert(title: Localization.string("alert.error.title"), message: error.localizedDescription) } return } @@ -145,19 +149,23 @@ func getAllAppVersionIdsFromServer(appId: String, ipaTool: IPATool) { let versionIds = json["data"] as! [Dictionary] if versionIds.count == 0 { DispatchQueue.main.async { - showAlert(title: "Error", message: "No version IDs, internal error maybe?") + showAlert(title: Localization.string("alert.error.title"), message: Localization.string("alert.version.none")) } return } DispatchQueue.main.async { let isiPad = UIDevice.current.userInterfaceIdiom == .pad - let alert = UIAlertController(title: "Select a version", message: "Select a version to downgrade to", preferredStyle: isiPad ? .alert : .actionSheet) + let alert = UIAlertController( + title: Localization.string("alert.version.select.title"), + message: Localization.string("alert.version.select.message"), + preferredStyle: isiPad ? .alert : .actionSheet + ) for versionId in versionIds { alert.addAction(UIAlertAction(title: "\(versionId["bundle_version"]!)", style: .default, handler: { _ in downgradeAppToVersion(appId: appId, versionId: "\(versionId["external_identifier"]!)", ipaTool: ipaTool) })) } - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + alert.addAction(UIAlertAction(title: Localization.string("action.cancel"), style: .cancel, handler: nil)) UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true, completion: nil) } } @@ -167,30 +175,38 @@ func getAllAppVersionIdsFromServer(appId: String, ipaTool: IPATool) { func downgradeApp(appId: String, ipaTool: IPATool) { @ObservedObject var appData = AppData.shared - let versionIds = ipaTool.getVersionIDList(appId: appId) - if versionIds.isEmpty { - print("No version ids were found, aborting...") + DispatchQueue.global(qos: .userInitiated).async { + let versionIds = ipaTool.getVersionIDList(appId: appId) DispatchQueue.main.async { - Alertinator.shared.alert(title: "Failed to downgrade app!", body: "Failed to get available version ids. This may be because the app has not been purchased yet, or there are no versions available to downgrade to.") - appData.isDowngrading = false - appData.appLink = "" - appData.applicationStatus = "Ready to Downgrade!" - appData.applicationIcon = "checkmark.circle.fill" + if versionIds.isEmpty { + print(Localization.string("log.download.noVersionIds")) + Alertinator.shared.alert( + title: Localization.string("alert.downgrade.failed.title"), + body: Localization.string("alert.downgrade.failed.message") + ) + appData.isDowngrading = false + appData.appLink = "" + appData.setStatus(.readyToDowngrade) + appData.applicationIcon = "checkmark.circle.fill" + return + } + + let isiPad = UIDevice.current.userInterfaceIdiom == .pad + let alert = UIAlertController( + title: Localization.string("alert.version.mode.title"), + message: Localization.string("alert.version.mode.message"), + preferredStyle: isiPad ? .alert : .actionSheet + ) + alert.addAction(UIAlertAction(title: Localization.string("alert.version.mode.manual"), style: .default, handler: { _ in + promptForVersionId(appId: appId, versionIds: versionIds, ipaTool: ipaTool) + })) + alert.addAction(UIAlertAction(title: Localization.string("alert.version.mode.server"), style: .default, handler: { _ in + getAllAppVersionIdsFromServer(appId: appId, ipaTool: ipaTool) + })) + alert.addAction(UIAlertAction(title: Localization.string("action.cancel"), style: .cancel, handler: nil)) + UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true, completion: nil) } - return } - - let isiPad = UIDevice.current.userInterfaceIdiom == .pad - - let alert = UIAlertController(title: "Version ID", message: "Do you want to enter the version ID manually or request the list of version IDs from the server?", preferredStyle: isiPad ? .alert : .actionSheet) - alert.addAction(UIAlertAction(title: "Manual", style: .default, handler: { _ in - promptForVersionId(appId: appId, versionIds: versionIds, ipaTool: ipaTool) - })) - alert.addAction(UIAlertAction(title: "Server", style: .default, handler: { _ in - getAllAppVersionIdsFromServer(appId: appId, ipaTool: ipaTool) - })) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true, completion: nil) } func cleanUp() { diff --git a/MuffinStoreJailed/Functions/IPATool.swift b/MuffinStoreJailed/Functions/IPATool.swift index 03bac0d..3025bed 100644 --- a/MuffinStoreJailed/Functions/IPATool.swift +++ b/MuffinStoreJailed/Functions/IPATool.swift @@ -109,10 +109,10 @@ class StoreClient { let authCookiesEnc1 = Data(base64Encoded: authCookiesEnc)! authCookies = NSKeyedUnarchiver.unarchiveObject(with: authCookiesEnc1) as? [HTTPCookie] pod = out["pod"] as? String - print("Loaded auth info") + print(Localization.string("log.auth.loadedInfo")) return true } - print("No auth info found, need to authenticate") + print(Localization.string("log.auth.notFound")) return false } @@ -207,7 +207,7 @@ class StoreClient { do { let resp = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as! [String: Any] if let dsPersonId = resp["dsPersonId"] as? String, let passwordToken = resp["passwordToken"] as? String, !dsPersonId.isEmpty, !passwordToken.isEmpty { - print("Authentication successful!") + print(Localization.string("log.auth.success")) appData.hasSent2FACode = true let download_queue_info = resp["download-queue-info"] as! [String: Any] let dsid = download_queue_info["dsid"] as! Int @@ -227,9 +227,13 @@ class StoreClient { self.saveAuthInfo() ret = true } else { - print("authentication failed: \(resp["customerMessage"] as! String)") + print(Localization.string("log.auth.failed", arguments: resp["customerMessage"] as? String ?? "")) DispatchQueue.main.async { - Alertinator.shared.alert(title: "Failed to log in!", body: "Make sure that your Apple ID and password are correct, and then try again.\n\nError: \(resp["customerMessage"] as! String)") + let errorMessage = resp["customerMessage"] as? String ?? "" + Alertinator.shared.alert( + title: Localization.string("alert.login.failed.title"), + body: Localization.string("alert.login.failed.message", arguments: errorMessage) + ) } } } catch { @@ -349,7 +353,7 @@ class IPATool { } func authenticate(requestCode: Bool = false) -> Bool { - print("Authenticating to iTunes Store...") + print(Localization.string("log.auth.authenticating")) if !storeClient.tryLoadAuthInfo() { return storeClient.authenticate(requestCode: requestCode) } else { @@ -358,22 +362,22 @@ class IPATool { } func getVersionIDList(appId: String) -> [String] { - print("Retrieving download info for appId \(appId)...") + print(Localization.string("log.download.retrievingInfo", arguments: appId)) let downResp = storeClient.download(appId: appId, isRedownload: true) let songList = downResp["songList"] as? [[String: Any]] ?? [] if songList.count == 0 { - print("Failed to get id list!") + print(Localization.string("log.download.failedIdList")) return [] } let downInfo = songList[0] let metadata = downInfo["metadata"] as? [String: Any] ?? [:] let appVerIds = metadata["softwareVersionExternalIdentifiers"] as? [Int] ?? [] - print("Got available version ids: \(appVerIds)") + print(Localization.string("log.download.gotVersionIds", arguments: appVerIds.description)) return appVerIds.map { String($0) } } func downloadIPAForVersion(appId: String, appVerId: String) -> String { - print("Downloading IPA for app \(appId) version \(appVerId)") + print(Localization.string("log.download.downloading", arguments: appId, appVerId)) let downResp = storeClient.download(appId: appId, appVer: appVerId) let songList = downResp["songList"] as! [[String: Any]] if songList.count == 0 { @@ -382,7 +386,7 @@ class IPATool { } let downInfo = songList[0] let url = downInfo["URL"] as! String - print("Got download URL: \(url)") + print(Localization.string("log.download.gotUrl", arguments: url)) let fm = FileManager.default let tempDir = fm.temporaryDirectory let path = tempDir.appendingPathComponent("app.ipa").path @@ -410,7 +414,7 @@ class IPATool { metadata["apple-id"] = appleId metadata["userName"] = appleId (metadata as NSDictionary).write(toFile: metadataPath, atomically: true) - print("Wrote iTunesMetadata.plist") + print(Localization.string("log.ipa.wroteMetadata")) var appContentDir = "" let payloadDir = unzipDirectory.appendingPathComponent("Payload") for entry in try! fm.contentsOfDirectory(atPath: payloadDir.path) { @@ -439,7 +443,7 @@ class IPATool { try! sinfData.write(to: unzipDirectory.appendingPathComponent(sinfPath)) print("Wrote sinf to \(sinfPath)") } - print("Downloaded IPA to \(unzipDirectory.path)") + print(Localization.string("log.ipa.downloaded", arguments: unzipDirectory.path)) return unzipDirectory.path } } @@ -447,7 +451,7 @@ class IPATool { class EncryptedKeychainWrapper { static func generateAndStoreKey() -> Void { self.deleteKey() - print("Generating key") + print(Localization.string("log.key.generating")) let query: [String: Any] = [ kSecClass as String: kSecClassKey, kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, @@ -466,13 +470,13 @@ class EncryptedKeychainWrapper { ] var error: Unmanaged? guard let privateKey = SecKeyCreateRandomKey(query as CFDictionary, &error) else { - print("Failed to generate key!!") + print(Localization.string("log.key.generateFailed")) return } - print("Generated key!") - print("Getting public key") + print(Localization.string("log.key.generated")) + print(Localization.string("log.key.gettingPublic")) let pubKey = SecKeyCopyPublicKey(privateKey)! - print("Got public key") + print(Localization.string("log.key.gotPublic")) let pubKeyData = SecKeyCopyExternalRepresentation(pubKey, &error)! as Data let pubKeyBase64 = pubKeyData.base64EncodedString() print("Public key: \(pubKeyBase64)") @@ -496,24 +500,24 @@ class EncryptedKeychainWrapper { var keyRef: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &keyRef) if status != errSecSuccess { - print("Failed to get key!") + print(Localization.string("log.key.getFailed")) return } - print("Got key!") + print(Localization.string("log.key.got")) let key = keyRef as! SecKey - print("Getting public key") + print(Localization.string("log.key.gettingPublic")) let pubKey = SecKeyCopyPublicKey(key)! - print("Got public key") - print("Encrypting data") + print(Localization.string("log.key.gotPublic")) + print(Localization.string("log.data.encrypting")) var error: Unmanaged? guard let encryptedData = SecKeyCreateEncryptedData(pubKey, .eciesEncryptionCofactorVariableIVX963SHA256AESGCM, base64.data(using: .utf8)! as CFData, &error) else { - print("Failed to encrypt data!") + print(Localization.string("log.data.encryptFailed")) return } - print("Encrypted data") + print(Localization.string("log.data.encrypted")) let path = fm.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("authinfo").path fm.createFile(atPath: path, contents: encryptedData as Data, attributes: nil) - print("Saved encrypted auth info") + print(Localization.string("log.auth.saved")) } static func loadAuthInfo() -> String? { @@ -533,22 +537,25 @@ class EncryptedKeychainWrapper { var keyRef: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &keyRef) if status != errSecSuccess { - print("Failed to get key! Aborting login...") + print(Localization.string("log.key.getFailed")) DispatchQueue.main.async { appData.appleId = "" appData.password = "" appData.hasSent2FACode = false - Alertinator.shared.alert(title: "Failed to login!", body: "Could not get the key. Please try again with a different Apple ID, preferrably one with 2FA enabled.") + Alertinator.shared.alert( + title: Localization.string("alert.login.keyFailed.title"), + body: Localization.string("alert.login.keyFailed.message") + ) } return nil } - print("Got key!") + print(Localization.string("log.key.got")) let key = keyRef as! SecKey let privKey = key - print("Decrypting data") + print(Localization.string("log.data.decrypting")) var error: Unmanaged? guard let decryptedData = SecKeyCreateDecryptedData(privKey, .eciesEncryptionCofactorVariableIVX963SHA256AESGCM, data as CFData, &error) else { - print("Failed to decrypt data!") + print(Localization.string("log.data.decryptFailed")) return nil } print("Decrypted data") diff --git a/MuffinStoreJailed/Localization/Localization.swift b/MuffinStoreJailed/Localization/Localization.swift new file mode 100644 index 0000000..f977016 --- /dev/null +++ b/MuffinStoreJailed/Localization/Localization.swift @@ -0,0 +1,52 @@ +import Foundation +import SwiftUI + +enum AppLanguage: String, CaseIterable, Identifiable { + case english = "en" + case russian = "ru" + + static let storageKey = "appLanguage" + + var id: String { rawValue } + var locale: Locale { Locale(identifier: rawValue) } + + var displayNameKey: LocalizedStringKey { + switch self { + case .english: + return LocalizedStringKey("language.english") + case .russian: + return LocalizedStringKey("language.russian") + } + } +} + +enum Localization { + static func currentLanguage() -> AppLanguage { + if let stored = UserDefaults.standard.string(forKey: AppLanguage.storageKey), + let language = AppLanguage(rawValue: stored) { + return language + } + return .english + } + + static func setLanguage(_ language: AppLanguage) { + guard currentLanguage() != language else { return } + UserDefaults.standard.set(language.rawValue, forKey: AppLanguage.storageKey) + } + + static func bundle(for language: AppLanguage? = nil) -> Bundle { + let targetLanguage = (language ?? currentLanguage()).rawValue + if let path = Bundle.main.path(forResource: targetLanguage, ofType: "lproj"), + let bundle = Bundle(path: path) { + return bundle + } + return .main + } + + static func string(_ key: String, arguments: CVarArg...) -> String { + let bundle = bundle() + let format = NSLocalizedString(key, tableName: nil, bundle: bundle, value: key, comment: "") + guard !arguments.isEmpty else { return format } + return String(format: format, locale: currentLanguage().locale, arguments: arguments) + } +} diff --git a/MuffinStoreJailed/MuffinStoreJailedApp.swift b/MuffinStoreJailed/MuffinStoreJailedApp.swift index e5b0dbf..cdc4951 100644 --- a/MuffinStoreJailed/MuffinStoreJailedApp.swift +++ b/MuffinStoreJailed/MuffinStoreJailedApp.swift @@ -17,6 +17,7 @@ struct MuffinStoreJailedApp: App { @StateObject private var appData = AppData.shared @AppStorage("autoCleanApp") var autoCleanApp: Bool = true + @AppStorage(AppLanguage.storageKey) private var languageCode: String = AppLanguage.english.rawValue init() { // Setup log stuff (redirect stdout) @@ -33,11 +34,18 @@ struct MuffinStoreJailedApp: App { WindowGroup { ContentView() .environmentObject(appData) + .environment(\.locale, Locale(identifier: languageCode)) .onAppear { + Localization.setLanguage(AppLanguage(rawValue: languageCode) ?? .english) if autoCleanApp { cleanUp() } } + .onChange(of: languageCode) { _ in + let language = AppLanguage(rawValue: languageCode) ?? .english + Localization.setLanguage(language) + appData.refreshLocalization() + } // receive the incoming url .onOpenURL { schemedURL in let rawURL = schemedURL.absoluteString.replacingOccurrences(of: "pancakestore:", with: "") diff --git a/MuffinStoreJailed/en.lproj/Localizable.strings b/MuffinStoreJailed/en.lproj/Localizable.strings new file mode 100644 index 0000000..8d88a2e --- /dev/null +++ b/MuffinStoreJailed/en.lproj/Localizable.strings @@ -0,0 +1,112 @@ +"app.name" = "PancakeStore"; +"status.notLoggedIn" = "Not logged in!"; +"status.readyToDowngrade" = "Ready to Downgrade!"; +"status.downgrading" = "Downgrading Application..."; +"status.downgradeSuccessful" = "Downgrade successful!"; +"alert.login.missingCredentials.title" = "No Apple ID details were input!"; +"alert.login.missingCredentials.message" = "Please type both your Apple ID email address & password, then try again."; +"action.logIn" = "Log In"; +"action.sendTwoFactor" = "Send 2FA Code"; +"action.openApp" = "Open App"; +"action.goHome" = "Go to Home Screen"; +"action.downgradeApp" = "Downgrade App"; +"action.exportIpa" = "Export IPA"; +"action.logOut" = "Log Out"; +"section.logs.title" = "Logs"; +"section.logs.footer" = "Originally created by [mineek](https://github.com/mineek) with backend and QoL enhancements made by [jailbreak.party](https://github.com/jailbreakdotparty).\n[Join the jailbreak.party discord!](https://jailbreak.party/discord)"; +"section.login.title" = "Login"; +"login.placeholder.appleId" = "Apple ID"; +"login.placeholder.password" = "Password"; +"section.verification.title" = "Verification Code"; +"login.placeholder.twoFactor" = "2FA Code"; +"section.downgrade.title" = "Downgrade App"; +"section.downgrade.footer" = "To downgrade an app, it must have been purchased on your account at some point in the past (when the app has a cloud icon next to it). It must also not be installed on your device currently, but you can offload it."; +"downgrade.placeholder.link" = "Link to App Store App"; +"section.appInfo.title" = "App Info"; +"info.label.appLink" = "App Link"; +"info.label.bundleId" = "App Bundle ID"; +"info.label.targetVersion" = "Target App Version"; +"action.copyValue" = "Copy Value"; +"action.copyOutput" = "Copy Output"; +"section.about.title" = "About"; +"action.discord" = "Discord"; +"action.github" = "GitHub"; +"action.website" = "Website"; +"section.settings.title" = "Settings"; +"settings.autoClean.title" = "Auto-Clean App"; +"settings.autoClean.subtitle" = "This is toggled on by default to make sure that PancakeStore doesn't keep any saved data from the app you had downgraded."; +"section.language.title" = "Language"; +"section.data.title" = "Data"; +"section.data.footer" = "If PancakeStore is taking up a lot of storage, click this button."; +"action.cleanDocuments" = "Clean Documents"; +"section.credits.title" = "Credits"; +"credits.mineek" = "Original Project, MuffinStore Jailed."; +"credits.lunginspector" = "UI changes and QoL improvements."; +"credits.skadz" = "Backend fixes."; +"settings.title" = "Settings"; +"language.english" = "English"; +"language.russian" = "Russian"; +"action.cancel" = "Cancel"; +"alert.version.enter.title" = "Enter version ID"; +"alert.version.enter.message" = "Select a version to downgrade to"; +"alert.error.title" = "Error"; +"alert.version.none" = "No version IDs, internal error maybe?"; +"alert.version.select.title" = "Select a version"; +"alert.version.select.message" = "Select a version to downgrade to"; +"alert.downgrade.failed.title" = "Failed to downgrade app!"; +"alert.downgrade.failed.message" = "Failed to get available version ids. This may be because the app has not been purchased yet, or there are no versions available to downgrade to."; +"alert.version.mode.title" = "Version ID"; +"alert.version.mode.message" = "Do you want to enter the version ID manually or request the list of version IDs from the server?"; +"alert.version.mode.manual" = "Manual"; +"alert.version.mode.server" = "Server"; +"alert.login.failed.title" = "Failed to log in!"; +"alert.login.failed.message" = "Make sure that your Apple ID and password are correct, and then try again.\n\nError: %@"; +"alert.login.keyFailed.title" = "Failed to login!"; +"alert.login.keyFailed.message" = "Could not get the key. Please try again with a different Apple ID, preferrably one with 2FA enabled."; +"action.ok" = "OK"; + +"log.keychain.foundAuth" = "Found auth info in keychain"; +"log.keychain.foundNoAuth" = "Found no auth info in keychain"; +"log.keychain.failedGet" = "Failed to get auth info from keychain, logging out"; +"log.reauth.success" = "Re-authenticated successfully"; +"log.reauth.failure" = "Re-authenticated unsuccessfully"; +"log.keychain.noAuthGenerating" = "No auth info found in keychain, setting up by generating a key in SEP"; + +"log.auth.loadedInfo" = "Loaded auth info"; +"log.auth.notFound" = "No auth info found, need to authenticate"; +"log.auth.authenticating" = "Authenticating to iTunes Store..."; +"log.auth.success" = "Authentication successful!"; +"log.auth.failed" = "Authentication failed: %@"; +"log.auth.saved" = "Saved encrypted auth info"; + +"log.key.generating" = "Generating key"; +"log.key.generated" = "Generated key!"; +"log.key.gettingPublic" = "Getting public key"; +"log.key.gotPublic" = "Got public key"; +"log.key.generateFailed" = "Failed to generate key!"; +"log.key.got" = "Got key!"; +"log.key.getFailed" = "Failed to get key!"; + +"log.data.encrypting" = "Encrypting data"; +"log.data.encrypted" = "Encrypted data"; +"log.data.encryptFailed" = "Failed to encrypt data!"; +"log.data.decrypting" = "Decrypting data"; +"log.data.decryptFailed" = "Failed to decrypt data!"; + +"log.download.failedIdList" = "Failed to get version id list!"; +"log.download.gotVersionIds" = "Got available version ids: %@"; +"log.download.noVersionIds" = "No version ids were found, aborting..."; +"log.download.retrievingInfo" = "Retrieving download info for appId %@..."; +"log.download.downloading" = "Downloading IPA for app %@ version %@"; +"log.download.gotUrl" = "Got download URL: %@"; + +"log.ipa.wroteMetadata" = "Wrote iTunesMetadata.plist"; +"log.ipa.downloaded" = "Downloaded IPA to %@"; +"log.ipa.downloadedTo" = "IPA downloaded to %@"; +"log.ipa.zippedTo" = "IPA zipped to %@"; + +"log.server.started" = "Server has started listening"; +"log.server.stopped" = "Server has stopped"; +"log.server.requestingInstall" = "Requesting app install"; +"log.server.servingIpa" = "Serving signed.ipa"; +"log.server.servingInstall" = "Serving install page"; diff --git a/MuffinStoreJailed/ru.lproj/Localizable.strings b/MuffinStoreJailed/ru.lproj/Localizable.strings new file mode 100644 index 0000000..845ac22 --- /dev/null +++ b/MuffinStoreJailed/ru.lproj/Localizable.strings @@ -0,0 +1,112 @@ +"app.name" = "PancakeStore"; +"status.notLoggedIn" = "Не выполнен вход!"; +"status.readyToDowngrade" = "Готово к даунгрейду!"; +"status.downgrading" = "Идёт даунгрейд приложения..."; +"status.downgradeSuccessful" = "Даунгрейд выполнен!"; +"alert.login.missingCredentials.title" = "Не введены данные Apple ID!"; +"alert.login.missingCredentials.message" = "Пожалуйста, введите адрес электронной почты и пароль Apple ID, затем попробуйте снова."; +"action.logIn" = "Войти"; +"action.sendTwoFactor" = "Отправить код 2FA"; +"action.openApp" = "Открыть приложение"; +"action.goHome" = "На домашний экран"; +"action.downgradeApp" = "Даунгрейд приложения"; +"action.exportIpa" = "Экспорт IPA"; +"action.logOut" = "Выйти"; +"section.logs.title" = "Логи"; +"section.logs.footer" = "Изначально создано [mineek](https://github.com/mineek) с доработками бэкенда и удобства от [jailbreak.party](https://github.com/jailbreakdotparty).\n[Вступайте на сервер jailbreak.party в Discord!](https://jailbreak.party/discord)"; +"section.login.title" = "Вход"; +"login.placeholder.appleId" = "Apple ID"; +"login.placeholder.password" = "Пароль"; +"section.verification.title" = "Проверочный код"; +"login.placeholder.twoFactor" = "Код 2FA"; +"section.downgrade.title" = "Даунгрейд приложения"; +"section.downgrade.footer" = "Чтобы выполнить даунгрейд, приложение должно быть ранее куплено на вашем аккаунте (иконка облака). Оно также не должно быть установлено на устройстве, но его можно выгрузить."; +"downgrade.placeholder.link" = "Ссылка на приложение в App Store"; +"section.appInfo.title" = "Информация о приложении"; +"info.label.appLink" = "Ссылка на приложение"; +"info.label.bundleId" = "Bundle ID"; +"info.label.targetVersion" = "Целевая версия приложения"; +"action.copyValue" = "Копировать значение"; +"action.copyOutput" = "Копировать вывод"; +"section.about.title" = "О приложении"; +"action.discord" = "Discord"; +"action.github" = "GitHub"; +"action.website" = "Веб‑сайт"; +"section.settings.title" = "Настройки"; +"settings.autoClean.title" = "Авто‑очистка приложения"; +"settings.autoClean.subtitle" = "Включено по умолчанию, чтобы PancakeStore не сохранял данные даунгрейднутого приложения."; +"section.language.title" = "Язык"; +"section.data.title" = "Данные"; +"section.data.footer" = "Если PancakeStore занимает много памяти, нажмите эту кнопку."; +"action.cleanDocuments" = "Очистить документы"; +"section.credits.title" = "Благодарности"; +"credits.mineek" = "Оригинальный проект MuffinStore Jailed."; +"credits.lunginspector" = "Изменения интерфейса и улучшения удобства."; +"credits.skadz" = "Исправления бэкенда."; +"settings.title" = "Настройки"; +"language.english" = "Английский"; +"language.russian" = "Русский"; +"action.cancel" = "Отмена"; +"alert.version.enter.title" = "Введите идентификатор версии"; +"alert.version.enter.message" = "Выберите версию для даунгрейда"; +"alert.error.title" = "Ошибка"; +"alert.version.none" = "Нет идентификаторов версий, возможно внутренняя ошибка?"; +"alert.version.select.title" = "Выберите версию"; +"alert.version.select.message" = "Выберите версию для даунгрейда"; +"alert.downgrade.failed.title" = "Не удалось выполнить даунгрейд!"; +"alert.downgrade.failed.message" = "Не удалось получить доступные идентификаторы версий. Возможно, приложение не было куплено или доступных версий нет."; +"alert.version.mode.title" = "Идентификатор версии"; +"alert.version.mode.message" = "Хотите ввести идентификатор версии вручную или запросить список версий с сервера?"; +"alert.version.mode.manual" = "Вручную"; +"alert.version.mode.server" = "Сервер"; +"alert.login.failed.title" = "Не удалось войти!"; +"alert.login.failed.message" = "Проверьте корректность Apple ID и пароля, затем попробуйте снова.\n\nОшибка: %@"; +"alert.login.keyFailed.title" = "Не удалось войти!"; +"alert.login.keyFailed.message" = "Не получилось получить ключ. Попробуйте другой Apple ID, желательно с включённой 2FA."; +"action.ok" = "ОК"; + +"log.keychain.foundAuth" = "Данные для входа найдены в связке ключей"; +"log.keychain.foundNoAuth" = "Данные для входа не найдены в связке ключей"; +"log.keychain.failedGet" = "Не удалось получить данные из связки ключей, выполняется выход"; +"log.reauth.success" = "Повторная аутентификация выполнена успешно"; +"log.reauth.failure" = "Повторная аутентификация не удалась"; +"log.keychain.noAuthGenerating" = "Данные не найдены в связке ключей, генерируем ключ в защищённом анклаве"; + +"log.auth.loadedInfo" = "Данные аутентификации загружены"; +"log.auth.notFound" = "Данные аутентификации не найдены, необходима аутентификация"; +"log.auth.authenticating" = "Аутентификация в iTunes Store..."; +"log.auth.success" = "Аутентификация успешна!"; +"log.auth.failed" = "Аутентификация не удалась: %@"; +"log.auth.saved" = "Зашифрованные данные аутентификации сохранены"; + +"log.key.generating" = "Генерация ключа"; +"log.key.generated" = "Ключ сгенерирован!"; +"log.key.gettingPublic" = "Получение публичного ключа"; +"log.key.gotPublic" = "Публичный ключ получен"; +"log.key.generateFailed" = "Не удалось сгенерировать ключ!"; +"log.key.got" = "Ключ получен!"; +"log.key.getFailed" = "Не удалось получить ключ!"; + +"log.data.encrypting" = "Шифрование данных"; +"log.data.encrypted" = "Данные зашифрованы"; +"log.data.encryptFailed" = "Не удалось зашифровать данные!"; +"log.data.decrypting" = "Расшифровка данных"; +"log.data.decryptFailed" = "Не удалось расшифровать данные!"; + +"log.download.failedIdList" = "Не удалось получить список версий!"; +"log.download.gotVersionIds" = "Получены доступные версии: %@"; +"log.download.noVersionIds" = "Идентификаторы версий не найдены, отмена..."; +"log.download.retrievingInfo" = "Получение информации о загрузке для appId %@..."; +"log.download.downloading" = "Загрузка IPA для приложения %@ версии %@"; +"log.download.gotUrl" = "Получен URL загрузки: %@"; + +"log.ipa.wroteMetadata" = "Записан iTunesMetadata.plist"; +"log.ipa.downloaded" = "IPA загружена в %@"; +"log.ipa.downloadedTo" = "IPA загружена в %@"; +"log.ipa.zippedTo" = "IPA заархивирована в %@"; + +"log.server.started" = "Сервер запущен и ожидает подключений"; +"log.server.stopped" = "Сервер остановлен"; +"log.server.requestingInstall" = "Запрос установки приложения"; +"log.server.servingIpa" = "Отдаём signed.ipa"; +"log.server.servingInstall" = "Отдаём страницу установки"; diff --git a/PancakeStore.xcodeproj/project.pbxproj b/PancakeStore.xcodeproj/project.pbxproj index 92d5adb..eef7fde 100644 --- a/PancakeStore.xcodeproj/project.pbxproj +++ b/PancakeStore.xcodeproj/project.pbxproj @@ -184,7 +184,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 2630; - LastUpgradeCheck = 1620; + LastUpgradeCheck = 2630; TargetAttributes = { 1561ED482F9F01DE008BEC23 = { CreatedOnToolsVersion = 26.3; @@ -200,6 +200,7 @@ knownRegions = ( en, Base, + ru, ); mainGroup = 6FA2A8DF2D24268F005CF73D; minimizedProjectReferenceProxies = 1; @@ -268,7 +269,6 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = HTH7RGGR9D; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MapleSyrup/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MapleSyrup; @@ -297,7 +297,6 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = HTH7RGGR9D; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MapleSyrup/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MapleSyrup; @@ -326,6 +325,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -356,6 +356,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = HTH7RGGR9D; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -379,6 +380,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; @@ -389,6 +391,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -419,6 +422,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = HTH7RGGR9D; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -435,6 +439,7 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; VALIDATE_PRODUCT = YES; }; @@ -446,9 +451,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = PancakeStore; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "Beta 1"; + CURRENT_PROJECT_VERSION = "Beta 2"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = HTH7RGGR9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "PancakeStore-Info.plist"; @@ -485,9 +489,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = PancakeStore; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "Beta 1"; + CURRENT_PROJECT_VERSION = "Beta 2"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = HTH7RGGR9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "PancakeStore-Info.plist"; diff --git a/PancakeStore.xcodeproj/xcshareddata/xcschemes/PancakeStore.xcscheme b/PancakeStore.xcodeproj/xcshareddata/xcschemes/PancakeStore.xcscheme index 87e8002..4a7ddb9 100644 --- a/PancakeStore.xcodeproj/xcshareddata/xcschemes/PancakeStore.xcscheme +++ b/PancakeStore.xcodeproj/xcshareddata/xcschemes/PancakeStore.xcscheme @@ -1,6 +1,6 @@