diff --git a/.gitignore b/.gitignore index 1c2162e..8f4bdb5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ Build/ DerivedData/ *.app +dist # Swift Package Manager .swiftpm/ @@ -27,6 +28,7 @@ xcuserdata/ *.swp *.swo *~ +.run # Temporary files *.tmp diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..81c7505 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,245 @@ +# Build and Release (Signed + Notarized DMG) + +This project includes `scripts/build_dmg.sh` to produce a distributable macOS DMG: + +- Output: `dist/Aether-.dmg` (`Aether-universal.dmg`, `Aether-arm64.dmg`, or `Aether-x86_64.dmg`) +- Default build target: universal (`arm64` + `x86_64`) +- Signs the `.app` and `.dmg` +- Notarizes and staples both (unless `--skip-notarization`) +- Generates checksum/manifest artifacts for verification publishing + +## What "can run on any computer" means on macOS + +For normal Gatekeeper-friendly distribution to other Macs, you need: + +1. A `Developer ID Application` signing certificate +2. Apple notarization +3. Stapled notarization ticket + +Without notarization/signing, users can still sometimes run the app by bypassing security prompts, but it is not a clean "works anywhere" install experience. + +## What notarization is + +Notarization is Apple scanning your signed app/archive for malicious content. If accepted, Apple issues a ticket. When you staple that ticket to your app/DMG, macOS can validate it offline and Gatekeeper is much less likely to block first launch. + +## Prerequisites + +1. macOS with Xcode Command Line Tools +2. Apple Developer Program membership (paid) +3. Installed `Developer ID Application` certificate in your keychain +4. `xcrun notarytool` credentials stored in keychain (profile name) + +## Apple ID, Team ID, and certificate details + +### Apple ID: can this be any email? + +No. It must be the Apple Account that has access to your Apple Developer team. In practice: + +- It can be any email address format only if that email is the sign-in for an Apple Account in the developer team +- For Apple ID auth with notarytool, you also need an app-specific password +- If you are in multiple teams with the same Apple ID, `--team-id` selects which team notarytool uses + +### Team ID: what is it? + +`TEAMID` is your Apple Developer Team identifier (usually 10 uppercase alphanumeric characters), for example `ABCDE12345`. + +Use the team that owns the `Developer ID Application` certificate used to sign the app. If team/certificate/auth do not match, notarization will fail. + +You can find Team ID in: + +- App Store Connect -> Users and Access -> Membership +- developer.apple.com -> Account -> Membership + +If you are in multiple teams, use the team that issued the `Developer ID Application` certificate you pass in `APP_SIGN_IDENTITY`. + +### Signing identity used by this script + +Set `APP_SIGN_IDENTITY` to your certificate common name, for example: + +```bash +APP_SIGN_IDENTITY="Developer ID Application: Your Name (ABCDE12345)" +``` + +You can inspect available code-sign identities with: + +```bash +security find-identity -v -p codesigning +``` + +## Configure notarytool credentials locally + +Create or update a local keychain profile (example profile name: `AETHER_NOTARY`): + +```bash +xcrun notarytool store-credentials AETHER_NOTARY \ + --apple-id "you@example.com" \ + --team-id "ABCDE12345" \ + --password "" \ + --validate +``` + +Notes: + +- `--password` is an Apple app-specific password, not your Apple Account login password +- If you omit `--password`, `notarytool` prompts securely in terminal +- Running `store-credentials` again with the same profile name updates/replaces the saved credentials + +## How to update stored credentials later + +Common cases: + +1. Password rotated/revoked: rerun `store-credentials` with the same profile name +2. Switched Apple ID or Team: rerun with new values under same or new profile name +3. Multiple environments: use separate profile names (for example `AETHER_NOTARY_DEV`, `AETHER_NOTARY_CI`) + +A quick validity check is to submit a build; `store-credentials --validate` also performs a credential validation request. + +## Build command + +From repo root: + +```bash +APP_SIGN_IDENTITY="Developer ID Application: Your Name (ABCDE12345)" \ +NOTARY_PROFILE="AETHER_NOTARY" \ +scripts/build_dmg.sh --bundle-id "com.yourcompany.aether" --version "1.2.1" --arch universal +``` + +If `--version` is omitted, the script uses the latest git tag (without leading `v`) or falls back to `0.0.0`. + +By default, the script sets: + +- `CFBundleShortVersionString` from `APP_VERSION` (what macOS shows as `Version X.Y.Z`) +- `CFBundleVersion` from `APP_BUILD` (defaults to git short hash) +- `AetherBuildTimestamp` from current UTC time +- `AetherBuildCommit` from git short hash +- `AetherBuildTargetArch` from `--arch` +- `AetherLicense` from `LICENSE_NAME` (default `MIT License`) + +So About shows `Version X.Y.Z ()` instead of duplicating the same value twice. + +## Useful script options + +```bash +scripts/build_dmg.sh --help +``` + +- `--bundle-id `: CFBundleIdentifier in `Info.plist` +- `--version `: app short/build version +- `--arch `: `universal` (default), `arm64`, or `x86_64` +- `--app-only`: build/sign only `dist/Aether-.app` (no DMG/notarization) +- `--open-app`: open the resulting app bundle after build +- `--skip-notarization`: sign only, skip notary submission/stapling + +Additional env var: + +- `APP_BUILD`: explicit build info for `CFBundleVersion` (for example `a1b2c3d4` or CI build number) +- `TARGET_ARCH`: same as `--arch` +- `BUILD_TIMESTAMP`: explicit UTC timestamp in ISO8601 format +- `BUILD_COMMIT`: explicit source marker (commit, tag, or CI revision) +- `LICENSE_NAME`: license label shown in About + +## Integrity artifacts generated + +For each build, the script writes: + +- `dist/Aether-.dmg` +- `dist/Aether-.dmg.sha256` +- `dist/Aether-.app-executable.sha256` +- `dist/Aether-.build-manifest.json` + +Recommended release publishing: + +1. Publish the DMG +2. Publish the `.dmg.sha256` and `.build-manifest.json` +3. Optionally publish the executable checksum file for in-app SHA comparison + +## Resource handling in the DMG build + +`scripts/build_dmg.sh` includes resources from both: + +- SwiftPM-generated resource bundle(s) (for `Bundle.module`, e.g. `Aether_Aether.bundle`) +- Source resources under `Sources/Aether/Resources` (copied to `Contents/Resources/AetherResources`) + +It also generates `Contents/Resources/AppIcon.icns` from: + +- `Sources/Aether/Resources/Assets.xcassets/AppIcon.appiconset` + +and sets `CFBundleIconFile=AppIcon` in the app `Info.plist` so Finder/Dock can use that icon. + +You do not need to move `Sources/Aether/Resources` for this packaging flow. + +## Local dry run (no Apple account required) + +For packaging flow validation only: + +```bash +APP_SIGN_IDENTITY="-" scripts/build_dmg.sh --skip-notarization +``` + +This uses ad-hoc signing and is not suitable for public distribution. + +## Run from IntelliJ as a real `.app` bundle + +`SwiftRunPackage` runs the executable directly, not from an `.app` bundle. +That is why App-menu behavior (including About integration) can differ from installed/package builds. + +Use the helper script to run the app as a bundle locally: + +```bash +scripts/run_app_bundle.sh +``` + +This defaults to your host architecture (`arm64` on Apple Silicon, `x86_64` on Intel), builds `dist/Aether-.app`, and launches it. + +To force universal: + +```bash +scripts/run_app_bundle.sh --arch universal +``` + +IntelliJ Run Configuration (recommended): + +1. Run | Edit Configurations... +2. Add New Configuration | Shell Script +3. Name: `Aether (Run App Bundle)` +4. Script path: `$PROJECT_DIR$/scripts/run_app_bundle.sh` +5. Working directory: `$PROJECT_DIR$` +6. Run + +Optional env vars in that config: + +- `APP_VERSION=1.2.1` (or your target version) + +By default, `scripts/run_app_bundle.sh` always uses ad-hoc signing (`-`) to avoid keychain/timestamp prompts. +If you need certificate signing for local runs, pass it explicitly: + +```bash +scripts/run_app_bundle.sh --sign-identity "Developer ID Application: Your Name (ABCDE12345)" +``` + +or set: + +```bash +RUN_APP_SIGN_IDENTITY="Developer ID Application: Your Name (ABCDE12345)" scripts/run_app_bundle.sh +``` + +## Verify output + +After build: + +```bash +ls -lh dist/Aether-*.dmg +cat dist/Aether-universal.dmg.sha256 +cat dist/Aether-universal.build-manifest.json +spctl -a -vvv -t open dist/Aether-universal.dmg +``` + +You can also mount and inspect: + +```bash +hdiutil attach dist/Aether-universal.dmg +``` + +## CI note + +Apple ID + app-specific password works, but App Store Connect API key auth is often preferred for CI (`notarytool store-credentials --key ... --key-id ... --issuer ...`). diff --git a/Package.swift b/Package.swift index 73e5745..972b0e0 100644 --- a/Package.swift +++ b/Package.swift @@ -3,6 +3,7 @@ import PackageDescription let package = Package( name: "Aether", + defaultLocalization: "en", platforms: [ .macOS(.v14) ], diff --git a/Sources/Aether/App/AetherApp.swift b/Sources/Aether/App/AetherApp.swift index 4703d13..ae7d09a 100644 --- a/Sources/Aether/App/AetherApp.swift +++ b/Sources/Aether/App/AetherApp.swift @@ -4,35 +4,44 @@ import SwiftUI struct AetherApp: App { @StateObject private var appState = AppState() @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + @AppStorage("appLanguage") private var appLanguage = AppLanguage.systemCode var body: some Scene { WindowGroup { MainView() + .id(appLanguage) .environmentObject(appState) + .environment(\.locale, AppLanguage.fromStored(appLanguage).locale) .preferredColorScheme(.dark) } .windowStyle(.hiddenTitleBar) .commands { + CommandGroup(replacing: .appInfo) { + Button(translate("about.menu.title")) { + AboutWindowManager.shared.open() + } + } + CommandGroup(replacing: .newItem) { - Button("Open Binary...") { + Button(translate("menu.file.openBinary")) { appState.openFile() } .keyboardShortcut("o", modifiers: .command) - Button("Open Project...") { + Button(translate("menu.file.openProject")) { appState.openProject() } .keyboardShortcut("o", modifiers: [.command, .shift]) Divider() - Button("Save Binary As...") { + Button(translate("menu.file.saveBinaryAs")) { appState.saveFileAs() } .keyboardShortcut("s", modifiers: .command) .disabled(appState.currentFile == nil) - Button("Save Project As...") { + Button(translate("menu.file.saveProjectAs")) { appState.saveProjectAs() } .keyboardShortcut("s", modifiers: [.command, .shift]) @@ -40,7 +49,7 @@ struct AetherApp: App { Divider() - Button("Close") { + Button(translate("menu.file.close")) { appState.closeFile() } .keyboardShortcut("w", modifiers: .command) @@ -48,26 +57,26 @@ struct AetherApp: App { } CommandGroup(replacing: .undoRedo) { - Button("Undo") { + Button(translate("menu.edit.undo")) { appState.undo() } .keyboardShortcut("z", modifiers: .command) .disabled(!appState.canUndo) - Button("Redo") { + Button(translate("menu.edit.redo")) { appState.redo() } .keyboardShortcut("z", modifiers: [.command, .shift]) .disabled(!appState.canRedo) } - CommandMenu("Analysis") { - Button("Analyze All") { + CommandMenu(translate("menu.analysis.title")) { + Button(translate("menu.analysis.analyzeAll")) { appState.analyzeAll() } .keyboardShortcut("a", modifiers: [.command, .shift]) .disabled(appState.currentFile == nil) - Button("Find Functions") { + Button(translate("menu.analysis.findFunctions")) { appState.findFunctions() } .keyboardShortcut("f", modifiers: [.command, .shift]) @@ -75,19 +84,19 @@ struct AetherApp: App { Divider() - Button("Show CFG") { + Button(translate("menu.analysis.showCFG")) { appState.showCFG = true } .keyboardShortcut("g", modifiers: .command) .disabled(appState.selectedFunction == nil) - Button("Decompile") { + Button(translate("menu.analysis.decompile")) { appState.decompileCurrentFunction() } .keyboardShortcut("d", modifiers: [.command, .shift]) .disabled(appState.selectedFunction == nil) - Button("Generate Pseudo-Code") { + Button(translate("menu.analysis.generatePseudoCode")) { appState.generateStructuredCode() } .keyboardShortcut("p", modifiers: [.command, .shift]) @@ -95,96 +104,96 @@ struct AetherApp: App { Divider() - Button("Call Graph") { + Button(translate("menu.analysis.callGraph")) { appState.showCallGraph = true } .keyboardShortcut("k", modifiers: .command) .disabled(appState.currentFile == nil) - Button("Crypto Detection") { + Button(translate("menu.analysis.cryptoDetection")) { appState.runCryptoDetection() } .disabled(appState.currentFile == nil) - Button("Deobfuscation Analysis") { + Button(translate("menu.analysis.deobfuscation")) { appState.runDeobfuscation() } .disabled(appState.selectedFunction == nil) - Button("Type Recovery") { + Button(translate("menu.analysis.typeRecovery")) { appState.runTypeRecovery() } .disabled(appState.selectedFunction == nil) - Button("Idiom Recognition") { + Button(translate("menu.analysis.idiomRecognition")) { appState.runIdiomRecognition() } .disabled(appState.selectedFunction == nil) Divider() - Button("Show Jump Table") { + Button(translate("menu.analysis.showJumpTable")) { appState.showJumpTable = true } .keyboardShortcut("j", modifiers: [.command, .shift]) .disabled(appState.selectedFunction == nil) } - CommandMenu("Export") { - Button("Export to IDA Python...") { + CommandMenu(translate("menu.export.title")) { + Button(translate("menu.export.ida")) { appState.showExportSheet = true } .disabled(appState.currentFile == nil) - Button("Export to Ghidra XML...") { + Button(translate("menu.export.ghidra")) { exportWithFormat(.ghidraXML) } .disabled(appState.currentFile == nil) - Button("Export to Radare2...") { + Button(translate("menu.export.radare2")) { exportWithFormat(.radare2) } .disabled(appState.currentFile == nil) - Button("Export to Binary Ninja...") { + Button(translate("menu.export.binaryNinja")) { exportWithFormat(.binaryNinja) } .disabled(appState.currentFile == nil) Divider() - Button("Export to JSON...") { + Button(translate("menu.export.json")) { exportWithFormat(.json) } .disabled(appState.currentFile == nil) - Button("Export to CSV...") { + Button(translate("menu.export.csv")) { exportWithFormat(.csv) } .disabled(appState.currentFile == nil) - Button("Export to HTML Report...") { + Button(translate("menu.export.html")) { exportWithFormat(.html) } .disabled(appState.currentFile == nil) - Button("Export to Markdown...") { + Button(translate("menu.export.markdown")) { exportWithFormat(.markdown) } .disabled(appState.currentFile == nil) - Button("Export C Header...") { + Button(translate("menu.export.cheader")) { exportWithFormat(.cHeader) } .disabled(appState.currentFile == nil) } - CommandMenu("Navigate") { - Button("Go to Address...") { + CommandMenu(translate("menu.navigate.title")) { + Button(translate("menu.navigate.gotoAddress")) { appState.showGoToAddress = true } .keyboardShortcut("g", modifiers: [.command, .shift]) - Button("Search...") { + Button(translate("menu.navigate.search")) { appState.showSearch = true } .keyboardShortcut("f", modifiers: .command) @@ -193,14 +202,16 @@ struct AetherApp: App { Settings { SettingsView() + .id(appLanguage) .environmentObject(appState) + .environment(\.locale, AppLanguage.fromStored(appLanguage).locale) } } private func exportWithFormat(_ format: ExportManager.ExportFormat) { let panel = NSSavePanel() panel.allowedContentTypes = [.data] - panel.nameFieldStringValue = "\(appState.currentFile?.name ?? "export").\(format.fileExtension)" + panel.nameFieldStringValue = "\(appState.currentFile?.name ?? translate("export.defaultFileName")).\(format.fileExtension)" if panel.runModal() == .OK, let url = panel.url { appState.exportTo(format: format, url: url) @@ -215,22 +226,22 @@ struct SettingsView: View { TabView { GeneralSettingsView() .tabItem { - Label("General", systemImage: "gear") + Label(translate("settings.tab.general"), systemImage: "gear") } AppearanceSettingsView() .tabItem { - Label("Appearance", systemImage: "paintbrush") + Label(translate("settings.tab.appearance"), systemImage: "paintbrush") } AnalysisSettingsView() .tabItem { - Label("Analysis", systemImage: "cpu") + Label(translate("settings.tab.analysis"), systemImage: "cpu") } AISettingsTab() .tabItem { - Label("AI", systemImage: "brain") + Label(translate("settings.tab.ai"), systemImage: "brain") } } .frame(width: 500, height: 350) @@ -240,13 +251,24 @@ struct SettingsView: View { struct GeneralSettingsView: View { @AppStorage("autoAnalyze") private var autoAnalyze = true @AppStorage("showHexView") private var showHexView = true + @AppStorage("appLanguage") private var appLanguage = AppLanguage.systemCode var body: some View { Form { - Toggle("Auto-analyze on file open", isOn: $autoAnalyze) - Toggle("Show Hex View by default", isOn: $showHexView) + Picker(translate("settings.general.language"), selection: $appLanguage) { + ForEach(AppLanguage.available) { language in + Text(language.pickerLabel).tag(language.code) + } + } + Toggle(translate("settings.general.autoAnalyze"), isOn: $autoAnalyze) + Toggle(translate("settings.general.showHexView"), isOn: $showHexView) } .padding() + .onAppear { + if !AppLanguage.isValidStorageValue(appLanguage) { + appLanguage = AppLanguage.systemCode + } + } } } @@ -256,7 +278,7 @@ struct AppearanceSettingsView: View { var body: some View { Form { - Picker("Font", selection: $fontName) { + Picker(translate("settings.appearance.font"), selection: $fontName) { Text("SF Mono").tag("SF Mono") Text("Menlo").tag("Menlo") Text("Monaco").tag("Monaco") @@ -264,7 +286,16 @@ struct AppearanceSettingsView: View { } Slider(value: $fontSize, in: 10...20, step: 1) { - Text("Font Size: \(Int(fontSize))") + Text(translate("settings.appearance.fontSize")) + } + + HStack { + Text(translate("settings.appearance.fontSize")) + .font(.caption) + .foregroundColor(.secondary) + Text("\(Int(fontSize))") + .font(.caption) + .foregroundColor(.secondary) } } .padding() @@ -278,9 +309,9 @@ struct AnalysisSettingsView: View { var body: some View { Form { - Toggle("Deep analysis (slower)", isOn: $deepAnalysis) - Toggle("Analyze strings", isOn: $analyzeStrings) - Toggle("Analyze cross-references", isOn: $analyzeXRefs) + Toggle(translate("settings.analysis.deepAnalysis"), isOn: $deepAnalysis) + Toggle(translate("settings.analysis.analyzeStrings"), isOn: $analyzeStrings) + Toggle(translate("settings.analysis.analyzeXrefs"), isOn: $analyzeXRefs) } .padding() } @@ -397,3 +428,30 @@ class AppDelegate: NSObject, NSApplicationDelegate { return image } } + +@MainActor +final class AboutWindowManager { + static let shared = AboutWindowManager() + private var window: NSWindow? + + func open() { + if let existingWindow = window { + existingWindow.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + return + } + + let controller = NSHostingController(rootView: AboutView()) + let aboutWindow = NSWindow(contentViewController: controller) + aboutWindow.title = translate("about.window.title") + aboutWindow.styleMask = [.titled, .closable, .miniaturizable, .resizable] + aboutWindow.setContentSize(NSSize(width: 620, height: 620)) + aboutWindow.contentMinSize = NSSize(width: 620, height: 620) + aboutWindow.center() + aboutWindow.isReleasedWhenClosed = false + + window = aboutWindow + aboutWindow.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + } +} diff --git a/Sources/Aether/App/AppLanguage.swift b/Sources/Aether/App/AppLanguage.swift new file mode 100644 index 0000000..348503f --- /dev/null +++ b/Sources/Aether/App/AppLanguage.swift @@ -0,0 +1,153 @@ +import Foundation + +struct AppLanguage: Identifiable, Hashable { + static let systemCode = "system" + static let system = AppLanguage(code: systemCode) + + let code: String + + var id: String { code } + + var locale: Locale { + isSystem ? .autoupdatingCurrent : Locale(identifier: code) + } + + var isSystem: Bool { + code == Self.systemCode + } + + var displayName: String { + if isSystem { + return translate("settings.language.system") + } + + return Self.localizedName(for: code) + } + + var flagEmoji: String { + if isSystem { + return "🌐" + } + + return Self.flagEmoji(forLanguageCode: code) ?? "🏳️" + } + + var pickerLabel: String { + "\(flagEmoji) \(displayName)" + } + + init(code: String) { + self.code = Self.normalizeLanguageCode(code) + } + + static var available: [AppLanguage] { + [system] + availableLanguageCodes.map { AppLanguage(code: $0) } + } + + static var supportedLanguageCodes: Set { + Set(availableLanguageCodes) + } + + static var fallbackLanguageCode: String? { + if let dev = Bundle.module.developmentLocalization.map(normalizeLanguageCode), + supportedLanguageCodes.contains(dev) { + return dev + } + + return availableLanguageCodes.first + } + + static func fromStored(_ storedValue: String?) -> AppLanguage { + guard let storedValue else { return .system } + + let normalized = normalizeLanguageCode(storedValue) + if normalized == systemCode { + return .system + } + + if supportedLanguageCodes.contains(normalized) { + return AppLanguage(code: normalized) + } + + return .system + } + + static func resolvedLanguageCode(for selectedLanguage: AppLanguage) -> String { + if !selectedLanguage.isSystem { + return selectedLanguage.code + } + + for preferred in Locale.preferredLanguages { + let base = normalizeLanguageCode(preferred) + if supportedLanguageCodes.contains(base) { + return base + } + } + + return fallbackLanguageCode ?? availableLanguageCodes.first ?? systemCode + } + + static func isValidStorageValue(_ storedValue: String) -> Bool { + let normalized = normalizeLanguageCode(storedValue) + return normalized == systemCode || supportedLanguageCodes.contains(normalized) + } + + private static var availableLanguageCodes: [String] { + let normalizedCodes = Bundle.module.localizations + .map(normalizeLanguageCode) + .filter { !$0.isEmpty && $0 != "base" && $0 != systemCode } + + let unique = Array(Set(normalizedCodes)) + return unique.sorted { localizedName(for: $0) < localizedName(for: $1) } + } + + private static func normalizeLanguageCode(_ identifier: String) -> String { + let normalized = identifier.replacingOccurrences(of: "_", with: "-").lowercased() + if normalized == systemCode { + return systemCode + } + + return String(normalized.split(separator: "-").first ?? Substring(normalized)) + } + + private static func localizedName(for code: String) -> String { + let locale = Locale.current + let localized = locale.localizedString(forLanguageCode: code) + ?? Locale(identifier: code).localizedString(forLanguageCode: code) + ?? code + + return localized.capitalized(with: locale) + } + + private static func flagEmoji(forLanguageCode languageCode: String) -> String? { + guard #available(macOS 13.0, *) else { return nil } + + let maximal = Locale.Language(identifier: languageCode).maximalIdentifier + let parts = maximal.split(separator: "-") + + guard let region = parts.last, region.count == 2 else { + return nil + } + + return flagEmoji(fromRegionCode: String(region)) + } + + private static func flagEmoji(fromRegionCode regionCode: String) -> String? { + let upper = regionCode.uppercased() + guard upper.count == 2, upper.unicodeScalars.allSatisfy({ $0.value >= 65 && $0.value <= 90 }) else { + return nil + } + + let base: UInt32 = 127_397 + var scalars = String.UnicodeScalarView() + + for scalar in upper.unicodeScalars { + guard let regional = UnicodeScalar(base + scalar.value) else { + return nil + } + scalars.append(regional) + } + + return String(scalars) + } +} diff --git a/Sources/Aether/App/Translate.swift b/Sources/Aether/App/Translate.swift new file mode 100644 index 0000000..8fcd2d6 --- /dev/null +++ b/Sources/Aether/App/Translate.swift @@ -0,0 +1,30 @@ +import Foundation + +func translate(_ key: String) -> String { + let rawLanguage = UserDefaults.standard.string(forKey: "appLanguage") + let selectedLanguage = AppLanguage.fromStored(rawLanguage) + let languageCode = AppLanguage.resolvedLanguageCode(for: selectedLanguage) + + let localized = localizationBundle(for: languageCode) + .localizedString(forKey: key, value: nil, table: "Localizable") + + if localized != key { + return localized + } + + if let fallbackCode = AppLanguage.fallbackLanguageCode, fallbackCode != languageCode { + return localizationBundle(for: fallbackCode) + .localizedString(forKey: key, value: key, table: "Localizable") + } + + return key +} + +private func localizationBundle(for languageCode: String) -> Bundle { + guard let path = Bundle.module.path(forResource: languageCode, ofType: "lproj"), + let bundle = Bundle(path: path) else { + return .module + } + + return bundle +} diff --git a/Sources/Aether/Resources/de.lproj/Localizable.strings b/Sources/Aether/Resources/de.lproj/Localizable.strings new file mode 100644 index 0000000..9c2f9e4 --- /dev/null +++ b/Sources/Aether/Resources/de.lproj/Localizable.strings @@ -0,0 +1,135 @@ +"common.error" = "Fehler"; +"common.ok" = "OK"; +"common.cancel" = "Abbrechen"; +"common.go" = "Los"; +"common.unknownError" = "Unbekannter Fehler"; + +"about.menu.title" = "Uber Aether"; +"about.window.title" = "Uber Aether"; +"about.tagline" = "Jenseits des Binars, zum Wesen."; +"about.version.format" = "Version %@ (%@)"; +"about.section.buildDetails" = "Build-Details"; +"about.field.built" = "Erstellt"; +"about.field.source" = "Quelle"; +"about.field.targetArch" = "Zielarchitektur"; +"about.field.runningArch" = "Laufende Architektur"; +"about.section.integrity" = "Integritats-Fingerabdruck"; +"about.action.copySha" = "SHA-256 kopieren"; +"about.action.copyReport" = "Build-Bericht kopieren"; +"about.section.license" = "Lizenz"; +"about.action.viewLicense" = "Lizenztext anzeigen"; +"about.verification.note" = "Vergleichen Sie den obenstehenden SHA-256-Wert mit den vom autoritativen Release-Urheber veroffentlichten Prufsummen, um diese ausfuhrbare Datei zu verifizieren."; +"about.report.title" = "Aether-Build-Bericht"; +"about.report.version" = "Version"; +"about.report.build" = "Build"; +"about.report.built" = "Erstellt"; +"about.report.source" = "Quelle"; +"about.report.targetArch" = "Zielarchitektur"; +"about.report.runningArch" = "Laufende Architektur"; +"about.report.license" = "Lizenz"; +"about.report.sha256" = "Ausfuhrbare SHA-256"; +"about.hash.calculating" = "Wird berechnet..."; +"about.hash.unavailable" = "Nicht verfugbar"; +"about.license.unavailable" = "Lizenztext ist in diesem Build nicht verfugbar."; + +"menu.file.openBinary" = "Binärdatei öffnen..."; +"menu.file.openProject" = "Projekt öffnen..."; +"menu.file.saveBinaryAs" = "Binärdatei speichern unter..."; +"menu.file.saveProjectAs" = "Projekt speichern unter..."; +"menu.file.close" = "Schließen"; +"menu.edit.undo" = "Rückgängig"; +"menu.edit.redo" = "Wiederholen"; +"menu.analysis.title" = "Analyse"; +"menu.analysis.analyzeAll" = "Alles analysieren"; +"menu.analysis.findFunctions" = "Funktionen finden"; +"menu.analysis.showCFG" = "CFG anzeigen"; +"menu.analysis.decompile" = "Dekompilieren"; +"menu.analysis.generatePseudoCode" = "Pseudocode generieren"; +"menu.analysis.callGraph" = "Aufrufgraph"; +"menu.analysis.cryptoDetection" = "Krypto-Erkennung"; +"menu.analysis.deobfuscation" = "Deobfuskationsanalyse"; +"menu.analysis.typeRecovery" = "Typrekonstruktion"; +"menu.analysis.idiomRecognition" = "Idiom-Erkennung"; +"menu.analysis.showJumpTable" = "Sprungtabelle anzeigen"; +"menu.export.title" = "Export"; +"menu.export.ida" = "Nach IDA Python exportieren..."; +"menu.export.ghidra" = "Nach Ghidra XML exportieren..."; +"menu.export.radare2" = "Nach Radare2 exportieren..."; +"menu.export.binaryNinja" = "Nach Binary Ninja exportieren..."; +"menu.export.json" = "Als JSON exportieren..."; +"menu.export.csv" = "Als CSV exportieren..."; +"menu.export.html" = "Als HTML-Bericht exportieren..."; +"menu.export.markdown" = "Als Markdown exportieren..."; +"menu.export.cheader" = "C-Header exportieren..."; +"menu.navigate.title" = "Navigation"; +"menu.navigate.gotoAddress" = "Gehe zu Adresse..."; +"menu.navigate.search" = "Suchen..."; + +"settings.tab.general" = "Allgemein"; +"settings.tab.appearance" = "Darstellung"; +"settings.tab.analysis" = "Analyse"; +"settings.tab.ai" = "KI"; +"settings.general.language" = "Sprache"; +"settings.language.system" = "System"; +"settings.language.english" = "Englisch"; +"settings.language.french" = "Französisch"; +"settings.language.german" = "Deutsch"; +"settings.language.italian" = "Italienisch"; +"settings.language.spanish" = "Spanisch"; +"settings.general.autoAnalyze" = "Beim Öffnen automatisch analysieren"; +"settings.general.showHexView" = "Hex-Ansicht standardmäßig anzeigen"; +"settings.appearance.font" = "Schriftart"; +"settings.appearance.fontSize" = "Schriftgröße"; +"settings.appearance.fontSizeValue %@" = "Schriftgröße: %@"; +"settings.analysis.deepAnalysis" = "Tiefenanalyse (langsamer)"; +"settings.analysis.analyzeStrings" = "Strings analysieren"; +"settings.analysis.analyzeXrefs" = "Querverweise analysieren"; + +"settings.ai.title" = "KI-Sicherheitsanalyse"; +"settings.ai.subtitle" = "Code mit KI auf Schwachstellen analysieren"; +"settings.ai.active" = "Aktiv"; +"settings.ai.apiKey" = "API-Schlüssel"; +"settings.ai.apiKeyPlaceholder" = "sk-ant-..."; +"settings.ai.apiKeyHelp" = "API-Schlüssel unter console.anthropic.com abrufen"; +"settings.ai.saveKey" = "Schlüssel speichern"; +"settings.ai.remove" = "Entfernen"; +"settings.ai.saved" = "Gespeichert!"; +"settings.ai.info.analyzes" = "Analysiert Code auf Sicherheitslücken"; +"settings.ai.info.keychain" = "API-Schlüssel wird im macOS-Schlüsselbund gespeichert"; +"settings.ai.info.credits" = "Verwendet Ihre API-Guthaben"; +"settings.ai.error.invalidFormat" = "Ungültiges Format"; +"settings.ai.error.saveFailed" = "Speichern fehlgeschlagen"; + +"welcome.title" = "Aether"; +"welcome.subtitle" = "Ziehen Sie eine Binärdatei hierher\noder verwenden Sie Datei → Binärdatei öffnen (⌘O)"; +"goto.title" = "Gehe zu Adresse"; +"goto.addressPlaceholder" = "Adresse (hex)"; + +"toolbar.open" = "Öffnen"; +"toolbar.save" = "Speichern"; +"toolbar.reload" = "Neu laden"; +"toolbar.close" = "Schließen"; +"toolbar.unsavedChanges" = "Ungespeicherte Änderungen"; +"toolbar.analyze" = "Analysieren"; +"toolbar.functions" = "Funktionen"; +"toolbar.decompiler" = "Dekompilierer"; +"toolbar.hexView" = "Hex-Ansicht"; +"toolbar.cfg" = "CFG"; +"toolbar.ai" = "KI"; +"toolbar.ai.chat" = "Mit KI chatten..."; +"toolbar.ai.explainFunction" = "Funktion erklären"; +"toolbar.ai.renameVariables" = "Variablen umbenennen"; +"toolbar.ai.securityAnalysis" = "Sicherheitsanalyse"; +"toolbar.ai.analyzeBinary" = "Binärdatei analysieren"; +"toolbar.ai.configure" = "API-Schlüssel in den Einstellungen konfigurieren"; +"toolbar.settings" = "Einstellungen"; +"toolbar.frida" = "Frida"; +"toolbar.frida.generateBasic" = "Basis-Skript generieren"; +"toolbar.frida.generateWithAI" = "Mit KI generieren"; +"toolbar.frida.hookMultiple" = "Mehrere Funktionen hooken"; +"toolbar.frida.platform" = "Plattform"; +"toolbar.frida.hookType" = "Hook-Typ"; +"toolbar.search" = "Suchen..."; +"toolbar.goto" = "Gehe zu"; + +"export.defaultFileName" = "export"; diff --git a/Sources/Aether/Resources/en.lproj/Localizable.strings b/Sources/Aether/Resources/en.lproj/Localizable.strings new file mode 100644 index 0000000..673389f --- /dev/null +++ b/Sources/Aether/Resources/en.lproj/Localizable.strings @@ -0,0 +1,135 @@ +"common.error" = "Error"; +"common.ok" = "OK"; +"common.cancel" = "Cancel"; +"common.go" = "Go"; +"common.unknownError" = "Unknown error"; + +"about.menu.title" = "About Aether"; +"about.window.title" = "About Aether"; +"about.tagline" = "Beyond the binary, into the essence."; +"about.version.format" = "Version %@ (%@)"; +"about.section.buildDetails" = "Build Details"; +"about.field.built" = "Built"; +"about.field.source" = "Source"; +"about.field.targetArch" = "Target Architecture"; +"about.field.runningArch" = "Running Architecture"; +"about.section.integrity" = "Integrity Fingerprint"; +"about.action.copySha" = "Copy SHA-256"; +"about.action.copyReport" = "Copy Build Report"; +"about.section.license" = "License"; +"about.action.viewLicense" = "View License Text"; +"about.verification.note" = "Compare the SHA-256 value above with checksums published by the authoritative release source to verify this executable."; +"about.report.title" = "Aether Build Report"; +"about.report.version" = "Version"; +"about.report.build" = "Build"; +"about.report.built" = "Built"; +"about.report.source" = "Source"; +"about.report.targetArch" = "Target Architecture"; +"about.report.runningArch" = "Running Architecture"; +"about.report.license" = "License"; +"about.report.sha256" = "Executable SHA-256"; +"about.hash.calculating" = "Calculating..."; +"about.hash.unavailable" = "Unavailable"; +"about.license.unavailable" = "License text is unavailable in this build."; + +"menu.file.openBinary" = "Open Binary..."; +"menu.file.openProject" = "Open Project..."; +"menu.file.saveBinaryAs" = "Save Binary As..."; +"menu.file.saveProjectAs" = "Save Project As..."; +"menu.file.close" = "Close"; +"menu.edit.undo" = "Undo"; +"menu.edit.redo" = "Redo"; +"menu.analysis.title" = "Analysis"; +"menu.analysis.analyzeAll" = "Analyze All"; +"menu.analysis.findFunctions" = "Find Functions"; +"menu.analysis.showCFG" = "Show CFG"; +"menu.analysis.decompile" = "Decompile"; +"menu.analysis.generatePseudoCode" = "Generate Pseudo-Code"; +"menu.analysis.callGraph" = "Call Graph"; +"menu.analysis.cryptoDetection" = "Crypto Detection"; +"menu.analysis.deobfuscation" = "Deobfuscation Analysis"; +"menu.analysis.typeRecovery" = "Type Recovery"; +"menu.analysis.idiomRecognition" = "Idiom Recognition"; +"menu.analysis.showJumpTable" = "Show Jump Table"; +"menu.export.title" = "Export"; +"menu.export.ida" = "Export to IDA Python..."; +"menu.export.ghidra" = "Export to Ghidra XML..."; +"menu.export.radare2" = "Export to Radare2..."; +"menu.export.binaryNinja" = "Export to Binary Ninja..."; +"menu.export.json" = "Export to JSON..."; +"menu.export.csv" = "Export to CSV..."; +"menu.export.html" = "Export to HTML Report..."; +"menu.export.markdown" = "Export to Markdown..."; +"menu.export.cheader" = "Export C Header..."; +"menu.navigate.title" = "Navigate"; +"menu.navigate.gotoAddress" = "Go to Address..."; +"menu.navigate.search" = "Search..."; + +"settings.tab.general" = "General"; +"settings.tab.appearance" = "Appearance"; +"settings.tab.analysis" = "Analysis"; +"settings.tab.ai" = "AI"; +"settings.general.language" = "Language"; +"settings.language.system" = "System"; +"settings.language.english" = "English"; +"settings.language.french" = "French"; +"settings.language.german" = "German"; +"settings.language.italian" = "Italian"; +"settings.language.spanish" = "Spanish"; +"settings.general.autoAnalyze" = "Auto-analyze on file open"; +"settings.general.showHexView" = "Show Hex View by default"; +"settings.appearance.font" = "Font"; +"settings.appearance.fontSize" = "Font Size"; +"settings.appearance.fontSizeValue %@" = "Font Size: %@"; +"settings.analysis.deepAnalysis" = "Deep analysis (slower)"; +"settings.analysis.analyzeStrings" = "Analyze strings"; +"settings.analysis.analyzeXrefs" = "Analyze cross-references"; + +"settings.ai.title" = "AI Security Analysis"; +"settings.ai.subtitle" = "Analyze code for vulnerabilities with AI"; +"settings.ai.active" = "Active"; +"settings.ai.apiKey" = "API Key"; +"settings.ai.apiKeyPlaceholder" = "sk-ant-..."; +"settings.ai.apiKeyHelp" = "Get your API key from console.anthropic.com"; +"settings.ai.saveKey" = "Save Key"; +"settings.ai.remove" = "Remove"; +"settings.ai.saved" = "Saved!"; +"settings.ai.info.analyzes" = "Analyzes code for security vulnerabilities"; +"settings.ai.info.keychain" = "API key stored in macOS Keychain"; +"settings.ai.info.credits" = "Uses your API credits"; +"settings.ai.error.invalidFormat" = "Invalid format"; +"settings.ai.error.saveFailed" = "Save failed"; + +"welcome.title" = "Aether"; +"welcome.subtitle" = "Drag and drop a binary file here\nor use File → Open Binary (⌘O)"; +"goto.title" = "Go to Address"; +"goto.addressPlaceholder" = "Address (hex)"; + +"toolbar.open" = "Open"; +"toolbar.save" = "Save"; +"toolbar.reload" = "Reload"; +"toolbar.close" = "Close"; +"toolbar.unsavedChanges" = "Unsaved changes"; +"toolbar.analyze" = "Analyze"; +"toolbar.functions" = "Functions"; +"toolbar.decompiler" = "Decompiler"; +"toolbar.hexView" = "Hex View"; +"toolbar.cfg" = "CFG"; +"toolbar.ai" = "AI"; +"toolbar.ai.chat" = "Chat with AI..."; +"toolbar.ai.explainFunction" = "Explain Function"; +"toolbar.ai.renameVariables" = "Rename Variables"; +"toolbar.ai.securityAnalysis" = "Security Analysis"; +"toolbar.ai.analyzeBinary" = "Analyze Binary"; +"toolbar.ai.configure" = "Configure API key in Settings"; +"toolbar.settings" = "Settings"; +"toolbar.frida" = "Frida"; +"toolbar.frida.generateBasic" = "Generate Basic Script"; +"toolbar.frida.generateWithAI" = "Generate with AI"; +"toolbar.frida.hookMultiple" = "Hook Multiple Functions"; +"toolbar.frida.platform" = "Platform"; +"toolbar.frida.hookType" = "Hook Type"; +"toolbar.search" = "Search..."; +"toolbar.goto" = "Go to"; + +"export.defaultFileName" = "export"; diff --git a/Sources/Aether/Resources/es.lproj/Localizable.strings b/Sources/Aether/Resources/es.lproj/Localizable.strings new file mode 100644 index 0000000..9d4de33 --- /dev/null +++ b/Sources/Aether/Resources/es.lproj/Localizable.strings @@ -0,0 +1,135 @@ +"common.error" = "Error"; +"common.ok" = "OK"; +"common.cancel" = "Cancelar"; +"common.go" = "Ir"; +"common.unknownError" = "Error desconocido"; + +"about.menu.title" = "Acerca de Aether"; +"about.window.title" = "Acerca de Aether"; +"about.tagline" = "Mas alla del binario, hacia la esencia."; +"about.version.format" = "Version %@ (%@)"; +"about.section.buildDetails" = "Detalles de compilacion"; +"about.field.built" = "Compilado"; +"about.field.source" = "Fuente"; +"about.field.targetArch" = "Arquitectura de destino"; +"about.field.runningArch" = "Arquitectura en ejecucion"; +"about.section.integrity" = "Huella de integridad"; +"about.action.copySha" = "Copiar SHA-256"; +"about.action.copyReport" = "Copiar informe de compilacion"; +"about.section.license" = "Licencia"; +"about.action.viewLicense" = "Ver texto de la licencia"; +"about.verification.note" = "Compara el valor SHA-256 de arriba con las sumas de verificacion publicadas por la fuente autorizada de lanzamiento para verificar este ejecutable."; +"about.report.title" = "Informe de compilacion de Aether"; +"about.report.version" = "Version"; +"about.report.build" = "Compilacion"; +"about.report.built" = "Compilado"; +"about.report.source" = "Fuente"; +"about.report.targetArch" = "Arquitectura de destino"; +"about.report.runningArch" = "Arquitectura en ejecucion"; +"about.report.license" = "Licencia"; +"about.report.sha256" = "SHA-256 del ejecutable"; +"about.hash.calculating" = "Calculando..."; +"about.hash.unavailable" = "No disponible"; +"about.license.unavailable" = "El texto de la licencia no esta disponible en esta compilacion."; + +"menu.file.openBinary" = "Abrir binario..."; +"menu.file.openProject" = "Abrir proyecto..."; +"menu.file.saveBinaryAs" = "Guardar binario como..."; +"menu.file.saveProjectAs" = "Guardar proyecto como..."; +"menu.file.close" = "Cerrar"; +"menu.edit.undo" = "Deshacer"; +"menu.edit.redo" = "Rehacer"; +"menu.analysis.title" = "Análisis"; +"menu.analysis.analyzeAll" = "Analizar todo"; +"menu.analysis.findFunctions" = "Buscar funciones"; +"menu.analysis.showCFG" = "Mostrar CFG"; +"menu.analysis.decompile" = "Decompilar"; +"menu.analysis.generatePseudoCode" = "Generar pseudo-código"; +"menu.analysis.callGraph" = "Grafo de llamadas"; +"menu.analysis.cryptoDetection" = "Detección criptográfica"; +"menu.analysis.deobfuscation" = "Análisis de desofuscación"; +"menu.analysis.typeRecovery" = "Recuperación de tipos"; +"menu.analysis.idiomRecognition" = "Reconocimiento de patrones"; +"menu.analysis.showJumpTable" = "Mostrar tabla de saltos"; +"menu.export.title" = "Exportar"; +"menu.export.ida" = "Exportar a IDA Python..."; +"menu.export.ghidra" = "Exportar a Ghidra XML..."; +"menu.export.radare2" = "Exportar a Radare2..."; +"menu.export.binaryNinja" = "Exportar a Binary Ninja..."; +"menu.export.json" = "Exportar a JSON..."; +"menu.export.csv" = "Exportar a CSV..."; +"menu.export.html" = "Exportar informe HTML..."; +"menu.export.markdown" = "Exportar a Markdown..."; +"menu.export.cheader" = "Exportar cabecera C..."; +"menu.navigate.title" = "Navegar"; +"menu.navigate.gotoAddress" = "Ir a dirección..."; +"menu.navigate.search" = "Buscar..."; + +"settings.tab.general" = "General"; +"settings.tab.appearance" = "Apariencia"; +"settings.tab.analysis" = "Análisis"; +"settings.tab.ai" = "IA"; +"settings.general.language" = "Idioma"; +"settings.language.system" = "Sistema"; +"settings.language.english" = "Inglés"; +"settings.language.french" = "Francés"; +"settings.language.german" = "Alemán"; +"settings.language.italian" = "Italiano"; +"settings.language.spanish" = "Español"; +"settings.general.autoAnalyze" = "Autoanalizar al abrir archivo"; +"settings.general.showHexView" = "Mostrar vista hex por defecto"; +"settings.appearance.font" = "Fuente"; +"settings.appearance.fontSize" = "Tamaño de fuente"; +"settings.appearance.fontSizeValue %@" = "Tamaño de fuente: %@"; +"settings.analysis.deepAnalysis" = "Análisis profundo (más lento)"; +"settings.analysis.analyzeStrings" = "Analizar cadenas"; +"settings.analysis.analyzeXrefs" = "Analizar referencias cruzadas"; + +"settings.ai.title" = "Análisis de seguridad con IA"; +"settings.ai.subtitle" = "Analiza código en busca de vulnerabilidades con IA"; +"settings.ai.active" = "Activo"; +"settings.ai.apiKey" = "Clave API"; +"settings.ai.apiKeyPlaceholder" = "sk-ant-..."; +"settings.ai.apiKeyHelp" = "Obtén tu clave API en console.anthropic.com"; +"settings.ai.saveKey" = "Guardar clave"; +"settings.ai.remove" = "Eliminar"; +"settings.ai.saved" = "¡Guardado!"; +"settings.ai.info.analyzes" = "Analiza código para detectar vulnerabilidades"; +"settings.ai.info.keychain" = "Clave API almacenada en Llavero de macOS"; +"settings.ai.info.credits" = "Usa tus créditos de API"; +"settings.ai.error.invalidFormat" = "Formato inválido"; +"settings.ai.error.saveFailed" = "Error al guardar"; + +"welcome.title" = "Aether"; +"welcome.subtitle" = "Arrastra y suelta un binario aquí\no usa Archivo → Abrir binario (⌘O)"; +"goto.title" = "Ir a dirección"; +"goto.addressPlaceholder" = "Dirección (hex)"; + +"toolbar.open" = "Abrir"; +"toolbar.save" = "Guardar"; +"toolbar.reload" = "Recargar"; +"toolbar.close" = "Cerrar"; +"toolbar.unsavedChanges" = "Cambios sin guardar"; +"toolbar.analyze" = "Analizar"; +"toolbar.functions" = "Funciones"; +"toolbar.decompiler" = "Decompilador"; +"toolbar.hexView" = "Vista Hex"; +"toolbar.cfg" = "CFG"; +"toolbar.ai" = "IA"; +"toolbar.ai.chat" = "Chatear con IA..."; +"toolbar.ai.explainFunction" = "Explicar función"; +"toolbar.ai.renameVariables" = "Renombrar variables"; +"toolbar.ai.securityAnalysis" = "Análisis de seguridad"; +"toolbar.ai.analyzeBinary" = "Analizar binario"; +"toolbar.ai.configure" = "Configurar clave API en Ajustes"; +"toolbar.settings" = "Ajustes"; +"toolbar.frida" = "Frida"; +"toolbar.frida.generateBasic" = "Generar script básico"; +"toolbar.frida.generateWithAI" = "Generar con IA"; +"toolbar.frida.hookMultiple" = "Hook de múltiples funciones"; +"toolbar.frida.platform" = "Plataforma"; +"toolbar.frida.hookType" = "Tipo de hook"; +"toolbar.search" = "Buscar..."; +"toolbar.goto" = "Ir a"; + +"export.defaultFileName" = "export"; diff --git a/Sources/Aether/Resources/fr.lproj/Localizable.strings b/Sources/Aether/Resources/fr.lproj/Localizable.strings new file mode 100644 index 0000000..169c138 --- /dev/null +++ b/Sources/Aether/Resources/fr.lproj/Localizable.strings @@ -0,0 +1,135 @@ +"common.error" = "Erreur"; +"common.ok" = "OK"; +"common.cancel" = "Annuler"; +"common.go" = "Aller"; +"common.unknownError" = "Erreur inconnue"; + +"about.menu.title" = "A propos d'Aether"; +"about.window.title" = "A propos d'Aether"; +"about.tagline" = "Au-dela du binaire, vers l'essence."; +"about.version.format" = "Version %@ (%@)"; +"about.section.buildDetails" = "Details de build"; +"about.field.built" = "Compile le"; +"about.field.source" = "Source"; +"about.field.targetArch" = "Architecture cible"; +"about.field.runningArch" = "Architecture en execution"; +"about.section.integrity" = "Empreinte d'integrite"; +"about.action.copySha" = "Copier SHA-256"; +"about.action.copyReport" = "Copier le rapport de build"; +"about.section.license" = "Licence"; +"about.action.viewLicense" = "Voir le texte de la licence"; +"about.verification.note" = "Comparez la valeur SHA-256 ci-dessus avec les sommes de controle publiees par la source de publication autoritative pour verifier cet executable."; +"about.report.title" = "Rapport de build Aether"; +"about.report.version" = "Version"; +"about.report.build" = "Build"; +"about.report.built" = "Compile le"; +"about.report.source" = "Source"; +"about.report.targetArch" = "Architecture cible"; +"about.report.runningArch" = "Architecture en execution"; +"about.report.license" = "Licence"; +"about.report.sha256" = "SHA-256 de l'executable"; +"about.hash.calculating" = "Calcul en cours..."; +"about.hash.unavailable" = "Indisponible"; +"about.license.unavailable" = "Le texte de la licence est indisponible dans ce build."; + +"menu.file.openBinary" = "Ouvrir un binaire..."; +"menu.file.openProject" = "Ouvrir un projet..."; +"menu.file.saveBinaryAs" = "Enregistrer le binaire sous..."; +"menu.file.saveProjectAs" = "Enregistrer le projet sous..."; +"menu.file.close" = "Fermer"; +"menu.edit.undo" = "Annuler"; +"menu.edit.redo" = "Rétablir"; +"menu.analysis.title" = "Analyse"; +"menu.analysis.analyzeAll" = "Analyser tout"; +"menu.analysis.findFunctions" = "Trouver les fonctions"; +"menu.analysis.showCFG" = "Afficher le CFG"; +"menu.analysis.decompile" = "Décompiler"; +"menu.analysis.generatePseudoCode" = "Générer le pseudo-code"; +"menu.analysis.callGraph" = "Graphe d'appels"; +"menu.analysis.cryptoDetection" = "Détection crypto"; +"menu.analysis.deobfuscation" = "Analyse de désobfuscation"; +"menu.analysis.typeRecovery" = "Récupération de types"; +"menu.analysis.idiomRecognition" = "Reconnaissance d'idiomes"; +"menu.analysis.showJumpTable" = "Afficher la table des sauts"; +"menu.export.title" = "Exporter"; +"menu.export.ida" = "Exporter vers IDA Python..."; +"menu.export.ghidra" = "Exporter vers Ghidra XML..."; +"menu.export.radare2" = "Exporter vers Radare2..."; +"menu.export.binaryNinja" = "Exporter vers Binary Ninja..."; +"menu.export.json" = "Exporter en JSON..."; +"menu.export.csv" = "Exporter en CSV..."; +"menu.export.html" = "Exporter en rapport HTML..."; +"menu.export.markdown" = "Exporter en Markdown..."; +"menu.export.cheader" = "Exporter l'en-tête C..."; +"menu.navigate.title" = "Navigation"; +"menu.navigate.gotoAddress" = "Aller à l'adresse..."; +"menu.navigate.search" = "Rechercher..."; + +"settings.tab.general" = "Général"; +"settings.tab.appearance" = "Apparence"; +"settings.tab.analysis" = "Analyse"; +"settings.tab.ai" = "IA"; +"settings.general.language" = "Langue"; +"settings.language.system" = "Système"; +"settings.language.english" = "Anglais"; +"settings.language.french" = "Français"; +"settings.language.german" = "Allemand"; +"settings.language.italian" = "Italien"; +"settings.language.spanish" = "Espagnol"; +"settings.general.autoAnalyze" = "Analyser automatiquement à l'ouverture"; +"settings.general.showHexView" = "Afficher la vue hexadécimale par défaut"; +"settings.appearance.font" = "Police"; +"settings.appearance.fontSize" = "Taille de police"; +"settings.appearance.fontSizeValue %@" = "Taille de police : %@"; +"settings.analysis.deepAnalysis" = "Analyse approfondie (plus lente)"; +"settings.analysis.analyzeStrings" = "Analyser les chaînes"; +"settings.analysis.analyzeXrefs" = "Analyser les références croisées"; + +"settings.ai.title" = "Analyse de sécurité IA"; +"settings.ai.subtitle" = "Analyser le code pour détecter des vulnérabilités avec l'IA"; +"settings.ai.active" = "Actif"; +"settings.ai.apiKey" = "Clé API"; +"settings.ai.apiKeyPlaceholder" = "sk-ant-..."; +"settings.ai.apiKeyHelp" = "Récupérez votre clé API sur console.anthropic.com"; +"settings.ai.saveKey" = "Enregistrer la clé"; +"settings.ai.remove" = "Supprimer"; +"settings.ai.saved" = "Enregistré !"; +"settings.ai.info.analyzes" = "Analyse le code pour les vulnérabilités"; +"settings.ai.info.keychain" = "Clé API stockée dans le trousseau macOS"; +"settings.ai.info.credits" = "Utilise vos crédits API"; +"settings.ai.error.invalidFormat" = "Format invalide"; +"settings.ai.error.saveFailed" = "Échec de l'enregistrement"; + +"welcome.title" = "Aether"; +"welcome.subtitle" = "Glissez-déposez un fichier binaire ici\nou utilisez Fichier → Ouvrir un binaire (⌘O)"; +"goto.title" = "Aller à l'adresse"; +"goto.addressPlaceholder" = "Adresse (hex)"; + +"toolbar.open" = "Ouvrir"; +"toolbar.save" = "Enregistrer"; +"toolbar.reload" = "Recharger"; +"toolbar.close" = "Fermer"; +"toolbar.unsavedChanges" = "Modifications non enregistrées"; +"toolbar.analyze" = "Analyser"; +"toolbar.functions" = "Fonctions"; +"toolbar.decompiler" = "Décompilateur"; +"toolbar.hexView" = "Vue Hex"; +"toolbar.cfg" = "CFG"; +"toolbar.ai" = "IA"; +"toolbar.ai.chat" = "Discuter avec l'IA..."; +"toolbar.ai.explainFunction" = "Expliquer la fonction"; +"toolbar.ai.renameVariables" = "Renommer les variables"; +"toolbar.ai.securityAnalysis" = "Analyse de sécurité"; +"toolbar.ai.analyzeBinary" = "Analyser le binaire"; +"toolbar.ai.configure" = "Configurer la clé API dans Réglages"; +"toolbar.settings" = "Réglages"; +"toolbar.frida" = "Frida"; +"toolbar.frida.generateBasic" = "Générer un script de base"; +"toolbar.frida.generateWithAI" = "Générer avec l'IA"; +"toolbar.frida.hookMultiple" = "Hooker plusieurs fonctions"; +"toolbar.frida.platform" = "Plateforme"; +"toolbar.frida.hookType" = "Type de hook"; +"toolbar.search" = "Rechercher..."; +"toolbar.goto" = "Aller à"; + +"export.defaultFileName" = "export"; diff --git a/Sources/Aether/Resources/it.lproj/Localizable.strings b/Sources/Aether/Resources/it.lproj/Localizable.strings new file mode 100644 index 0000000..997242e --- /dev/null +++ b/Sources/Aether/Resources/it.lproj/Localizable.strings @@ -0,0 +1,135 @@ +"common.error" = "Errore"; +"common.ok" = "OK"; +"common.cancel" = "Annulla"; +"common.go" = "Vai"; +"common.unknownError" = "Errore sconosciuto"; + +"about.menu.title" = "Informazioni su Aether"; +"about.window.title" = "Informazioni su Aether"; +"about.tagline" = "Oltre il binario, verso l'essenza."; +"about.version.format" = "Versione %@ (%@)"; +"about.section.buildDetails" = "Dettagli build"; +"about.field.built" = "Compilato"; +"about.field.source" = "Sorgente"; +"about.field.targetArch" = "Architettura di destinazione"; +"about.field.runningArch" = "Architettura in esecuzione"; +"about.section.integrity" = "Impronta di integrita"; +"about.action.copySha" = "Copia SHA-256"; +"about.action.copyReport" = "Copia report build"; +"about.section.license" = "Licenza"; +"about.action.viewLicense" = "Visualizza testo licenza"; +"about.verification.note" = "Confronta il valore SHA-256 sopra con i checksum pubblicati dalla fonte di rilascio autorevole per verificare questo eseguibile."; +"about.report.title" = "Report build Aether"; +"about.report.version" = "Versione"; +"about.report.build" = "Build"; +"about.report.built" = "Compilato"; +"about.report.source" = "Sorgente"; +"about.report.targetArch" = "Architettura di destinazione"; +"about.report.runningArch" = "Architettura in esecuzione"; +"about.report.license" = "Licenza"; +"about.report.sha256" = "SHA-256 dell'eseguibile"; +"about.hash.calculating" = "Calcolo in corso..."; +"about.hash.unavailable" = "Non disponibile"; +"about.license.unavailable" = "Il testo della licenza non e disponibile in questa build."; + +"menu.file.openBinary" = "Apri binario..."; +"menu.file.openProject" = "Apri progetto..."; +"menu.file.saveBinaryAs" = "Salva binario con nome..."; +"menu.file.saveProjectAs" = "Salva progetto con nome..."; +"menu.file.close" = "Chiudi"; +"menu.edit.undo" = "Annulla"; +"menu.edit.redo" = "Ripeti"; +"menu.analysis.title" = "Analisi"; +"menu.analysis.analyzeAll" = "Analizza tutto"; +"menu.analysis.findFunctions" = "Trova funzioni"; +"menu.analysis.showCFG" = "Mostra CFG"; +"menu.analysis.decompile" = "Decompila"; +"menu.analysis.generatePseudoCode" = "Genera pseudo-codice"; +"menu.analysis.callGraph" = "Grafo delle chiamate"; +"menu.analysis.cryptoDetection" = "Rilevamento crittografia"; +"menu.analysis.deobfuscation" = "Analisi di deoffuscamento"; +"menu.analysis.typeRecovery" = "Recupero tipi"; +"menu.analysis.idiomRecognition" = "Riconoscimento idiomi"; +"menu.analysis.showJumpTable" = "Mostra tabella salti"; +"menu.export.title" = "Esporta"; +"menu.export.ida" = "Esporta in IDA Python..."; +"menu.export.ghidra" = "Esporta in Ghidra XML..."; +"menu.export.radare2" = "Esporta in Radare2..."; +"menu.export.binaryNinja" = "Esporta in Binary Ninja..."; +"menu.export.json" = "Esporta in JSON..."; +"menu.export.csv" = "Esporta in CSV..."; +"menu.export.html" = "Esporta in report HTML..."; +"menu.export.markdown" = "Esporta in Markdown..."; +"menu.export.cheader" = "Esporta header C..."; +"menu.navigate.title" = "Naviga"; +"menu.navigate.gotoAddress" = "Vai all'indirizzo..."; +"menu.navigate.search" = "Cerca..."; + +"settings.tab.general" = "Generale"; +"settings.tab.appearance" = "Aspetto"; +"settings.tab.analysis" = "Analisi"; +"settings.tab.ai" = "IA"; +"settings.general.language" = "Lingua"; +"settings.language.system" = "Sistema"; +"settings.language.english" = "Inglese"; +"settings.language.french" = "Francese"; +"settings.language.german" = "Tedesco"; +"settings.language.italian" = "Italiano"; +"settings.language.spanish" = "Spagnolo"; +"settings.general.autoAnalyze" = "Analizza automaticamente all'apertura"; +"settings.general.showHexView" = "Mostra vista esadecimale per default"; +"settings.appearance.font" = "Font"; +"settings.appearance.fontSize" = "Dimensione font"; +"settings.appearance.fontSizeValue %@" = "Dimensione font: %@"; +"settings.analysis.deepAnalysis" = "Analisi approfondita (più lenta)"; +"settings.analysis.analyzeStrings" = "Analizza stringhe"; +"settings.analysis.analyzeXrefs" = "Analizza riferimenti incrociati"; + +"settings.ai.title" = "Analisi di sicurezza IA"; +"settings.ai.subtitle" = "Analizza il codice per vulnerabilità con IA"; +"settings.ai.active" = "Attivo"; +"settings.ai.apiKey" = "Chiave API"; +"settings.ai.apiKeyPlaceholder" = "sk-ant-..."; +"settings.ai.apiKeyHelp" = "Ottieni la tua chiave API da console.anthropic.com"; +"settings.ai.saveKey" = "Salva chiave"; +"settings.ai.remove" = "Rimuovi"; +"settings.ai.saved" = "Salvata!"; +"settings.ai.info.analyzes" = "Analizza il codice per vulnerabilità di sicurezza"; +"settings.ai.info.keychain" = "Chiave API salvata nel Portachiavi macOS"; +"settings.ai.info.credits" = "Usa i tuoi crediti API"; +"settings.ai.error.invalidFormat" = "Formato non valido"; +"settings.ai.error.saveFailed" = "Salvataggio non riuscito"; + +"welcome.title" = "Aether"; +"welcome.subtitle" = "Trascina qui un file binario\no usa File → Apri binario (⌘O)"; +"goto.title" = "Vai all'indirizzo"; +"goto.addressPlaceholder" = "Indirizzo (hex)"; + +"toolbar.open" = "Apri"; +"toolbar.save" = "Salva"; +"toolbar.reload" = "Ricarica"; +"toolbar.close" = "Chiudi"; +"toolbar.unsavedChanges" = "Modifiche non salvate"; +"toolbar.analyze" = "Analizza"; +"toolbar.functions" = "Funzioni"; +"toolbar.decompiler" = "Decompilatore"; +"toolbar.hexView" = "Vista Hex"; +"toolbar.cfg" = "CFG"; +"toolbar.ai" = "IA"; +"toolbar.ai.chat" = "Chatta con IA..."; +"toolbar.ai.explainFunction" = "Spiega funzione"; +"toolbar.ai.renameVariables" = "Rinomina variabili"; +"toolbar.ai.securityAnalysis" = "Analisi di sicurezza"; +"toolbar.ai.analyzeBinary" = "Analizza binario"; +"toolbar.ai.configure" = "Configura la chiave API nelle Impostazioni"; +"toolbar.settings" = "Impostazioni"; +"toolbar.frida" = "Frida"; +"toolbar.frida.generateBasic" = "Genera script base"; +"toolbar.frida.generateWithAI" = "Genera con IA"; +"toolbar.frida.hookMultiple" = "Hook di più funzioni"; +"toolbar.frida.platform" = "Piattaforma"; +"toolbar.frida.hookType" = "Tipo di hook"; +"toolbar.search" = "Cerca..."; +"toolbar.goto" = "Vai a"; + +"export.defaultFileName" = "export"; diff --git a/Sources/Aether/UI/About/AboutView.swift b/Sources/Aether/UI/About/AboutView.swift new file mode 100644 index 0000000..428fb03 --- /dev/null +++ b/Sources/Aether/UI/About/AboutView.swift @@ -0,0 +1,333 @@ +import SwiftUI +import CryptoKit +import AppKit + +struct AboutView: View { + @State private var executableSHA256 = translate("about.hash.calculating") + @State private var isHashing = false + @State private var showLicenseSheet = false + @State private var licenseText = translate("about.license.unavailable") + @State private var didLoad = false + + private let buildInfo = BuildInfo.current + private let primaryText = Color(red: 0.95, green: 0.97, blue: 1.00) + private let secondaryText = Color(red: 0.76, green: 0.82, blue: 0.92) + private let accentText = Color(red: 0.66, green: 0.78, blue: 0.98) + + var body: some View { + ZStack { + LinearGradient( + colors: [ + Color(red: 0.08, green: 0.10, blue: 0.16), + Color(red: 0.04, green: 0.05, blue: 0.09) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 16) { + header + buildDetailsCard + integrityCard + licenseCard + verificationNote + } + .padding(20) + .foregroundStyle(primaryText) + } + } + .frame(width: 620, height: 620) + .preferredColorScheme(.dark) + .onAppear { + guard !didLoad else { return } + didLoad = true + loadLicenseText() + refreshExecutableHash() + } + .sheet(isPresented: $showLicenseSheet) { + LicenseTextView(licenseName: buildInfo.license, licenseText: licenseText) + } + } + + private var header: some View { + HStack(alignment: .center, spacing: 16) { + Group { + if let appIcon = NSApp.applicationIconImage { + Image(nsImage: appIcon) + .resizable() + } else { + Image(systemName: "cpu.fill") + .resizable() + .scaledToFit() + .foregroundStyle(.blue) + .padding(20) + } + } + .frame(width: 80, height: 80) + .background( + RoundedRectangle(cornerRadius: 18, style: .continuous) + .fill(Color.white.opacity(0.07)) + ) + .overlay( + RoundedRectangle(cornerRadius: 18, style: .continuous) + .stroke(Color.white.opacity(0.12), lineWidth: 1) + ) + + VStack(alignment: .leading, spacing: 5) { + Text("Aether") + .font(.system(size: 34, weight: .bold, design: .rounded)) + Text(translate("about.tagline")) + .font(.subheadline) + .foregroundStyle(secondaryText) + Text(String(format: translate("about.version.format"), buildInfo.version, buildInfo.build)) + .font(.system(.headline, design: .monospaced)) + .foregroundStyle(accentText) + } + + Spacer(minLength: 0) + } + .padding(16) + .background(cardBackground) + } + + private var buildDetailsCard: some View { + VStack(alignment: .leading, spacing: 10) { + Label(translate("about.section.buildDetails"), systemImage: "wrench.and.screwdriver") + .font(.headline) + + AboutRow(label: translate("about.field.built"), value: buildInfo.timestampDisplay) + AboutRow(label: translate("about.field.source"), value: buildInfo.commit) + AboutRow(label: translate("about.field.targetArch"), value: buildInfo.targetArchitecture) + AboutRow(label: translate("about.field.runningArch"), value: buildInfo.runningArchitecture) + } + .padding(16) + .background(cardBackground) + } + + private var integrityCard: some View { + VStack(alignment: .leading, spacing: 10) { + Label(translate("about.section.integrity"), systemImage: "checkmark.shield") + .font(.headline) + + Text(executableSHA256) + .font(.system(.footnote, design: .monospaced)) + .textSelection(.enabled) + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(Color.black.opacity(0.28)) + ) + + HStack(spacing: 10) { + Button(translate("about.action.copySha")) { + copyToClipboard(executableSHA256) + } + .disabled(isHashing) + .buttonStyle(.bordered) + + Button(translate("about.action.copyReport")) { + copyToClipboard(buildReport) + } + .disabled(isHashing) + .buttonStyle(.bordered) + } + + if isHashing { + ProgressView() + .progressViewStyle(.circular) + } + } + .padding(16) + .background(cardBackground) + } + + private var licenseCard: some View { + VStack(alignment: .leading, spacing: 10) { + HStack { + Label(translate("about.section.license"), systemImage: "doc.text") + .font(.headline) + Spacer(minLength: 0) + Button(translate("about.action.viewLicense")) { + showLicenseSheet = true + } + .buttonStyle(.bordered) + } + + Text(buildInfo.license) + .font(.callout) + .foregroundStyle(secondaryText) + } + .padding(16) + .background(cardBackground) + } + + private var verificationNote: some View { + Text(translate("about.verification.note")) + .font(.footnote) + .foregroundStyle(secondaryText) + .padding(.horizontal, 4) + } + + private var buildReport: String { + """ + \(translate("about.report.title")) + \(translate("about.report.version")): \(buildInfo.version) + \(translate("about.report.build")): \(buildInfo.build) + \(translate("about.report.built")): \(buildInfo.timestampRaw) + \(translate("about.report.source")): \(buildInfo.commit) + \(translate("about.report.targetArch")): \(buildInfo.targetArchitecture) + \(translate("about.report.runningArch")): \(buildInfo.runningArchitecture) + \(translate("about.report.license")): \(buildInfo.license) + \(translate("about.report.sha256")): \(executableSHA256) + """ + } + + private var cardBackground: some View { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(Color.white.opacity(0.09)) + .overlay( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .stroke(Color.white.opacity(0.14), lineWidth: 1) + ) + } + + private func refreshExecutableHash() { + guard !isHashing else { return } + isHashing = true + + DispatchQueue.global(qos: .utility).async { + let hash = Self.computeExecutableSHA256() ?? translate("about.hash.unavailable") + DispatchQueue.main.async { + executableSHA256 = hash + isHashing = false + } + } + } + + private func loadLicenseText() { + guard let licenseURL = Bundle.main.url(forResource: "LICENSE", withExtension: "txt"), + let text = try? String(contentsOf: licenseURL, encoding: .utf8) else { + return + } + licenseText = text + } + + private static func computeExecutableSHA256() -> String? { + guard let executableURL = Bundle.main.executableURL, + let data = try? Data(contentsOf: executableURL, options: .mappedIfSafe) else { + return nil + } + + let digest = SHA256.hash(data: data) + return digest.map { String(format: "%02x", $0) }.joined() + } + + private func copyToClipboard(_ text: String) { + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(text, forType: .string) + } +} + +private struct AboutRow: View { + let label: String + let value: String + + var body: some View { + HStack(alignment: .firstTextBaseline, spacing: 12) { + Text(label) + .font(.subheadline.weight(.semibold)) + .foregroundStyle(Color(red: 0.76, green: 0.82, blue: 0.92)) + .frame(width: 150, alignment: .leading) + + Text(value) + .font(.system(.subheadline, design: .monospaced)) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + } + } +} + +private struct LicenseTextView: View { + let licenseName: String + let licenseText: String + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack(spacing: 12) { + HStack { + Text(licenseName) + .font(.title3.bold()) + Spacer() + Button("Close") { + dismiss() + } + } + + ScrollView { + Text(licenseText) + .font(.system(.body, design: .monospaced)) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(12) + } + .background( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(Color.black.opacity(0.20)) + ) + } + .padding(16) + .frame(minWidth: 680, minHeight: 520) + .background(Color(red: 0.09, green: 0.10, blue: 0.14)) + .preferredColorScheme(.dark) + } +} + +private struct BuildInfo { + let version: String + let build: String + let commit: String + let timestampRaw: String + let targetArchitecture: String + let runningArchitecture: String + let license: String + + var timestampDisplay: String { + let formatter = ISO8601DateFormatter() + guard let date = formatter.date(from: timestampRaw) else { + return timestampRaw + } + + let localFormatter = DateFormatter() + localFormatter.dateStyle = .medium + localFormatter.timeStyle = .medium + localFormatter.timeZone = .current + return "\(localFormatter.string(from: date)) (\(timestampRaw))" + } + + static var current: BuildInfo { + let info = Bundle.main.infoDictionary ?? [:] + return BuildInfo( + version: info["CFBundleShortVersionString"] as? String ?? "unknown", + build: info["CFBundleVersion"] as? String ?? "unknown", + commit: info["AetherBuildCommit"] as? String ?? "unknown", + timestampRaw: info["AetherBuildTimestamp"] as? String ?? "unknown", + targetArchitecture: info["AetherBuildTargetArch"] as? String ?? "unknown", + runningArchitecture: runtimeArchitecture, + license: info["AetherLicense"] as? String ?? "MIT License" + ) + } + + private static var runtimeArchitecture: String { +#if arch(arm64) + "arm64" +#elseif arch(x86_64) + "x86_64" +#else + "unknown" +#endif + } +} diff --git a/Sources/Aether/UI/MainWindow/MainView.swift b/Sources/Aether/UI/MainWindow/MainView.swift index ce587d0..88afb73 100644 --- a/Sources/Aether/UI/MainWindow/MainView.swift +++ b/Sources/Aether/UI/MainWindow/MainView.swift @@ -134,12 +134,12 @@ struct MainView: View { WelcomeView() } } - .alert("Error", isPresented: $appState.showError) { - Button("OK") { + .alert(translate("common.error"), isPresented: $appState.showError) { + Button(translate("common.ok")) { appState.showError = false } } message: { - Text(appState.errorMessage ?? "Unknown error") + Text(appState.errorMessage ?? translate("common.unknownError")) } } @@ -175,11 +175,11 @@ struct WelcomeView: View { .font(.system(size: 64)) .foregroundColor(.secondary) - Text("Aether") + Text(translate("welcome.title")) .font(.largeTitle) .fontWeight(.bold) - Text("Drag and drop a binary file here\nor use File → Open Binary (⌘O)") + Text(translate("welcome.subtitle")) .multilineTextAlignment(.center) .foregroundColor(.secondary) @@ -253,10 +253,10 @@ struct GoToAddressSheet: View { var body: some View { VStack(spacing: 16) { - Text("Go to Address") + Text(translate("goto.title")) .font(.headline) - TextField("Address (hex)", text: $addressText) + TextField(translate("goto.addressPlaceholder"), text: $addressText) .textFieldStyle(.roundedBorder) .focused($isFocused) .onSubmit { @@ -264,12 +264,12 @@ struct GoToAddressSheet: View { } HStack { - Button("Cancel") { + Button(translate("common.cancel")) { dismiss() } .keyboardShortcut(.cancelAction) - Button("Go") { + Button(translate("common.go")) { goToAddress() } .keyboardShortcut(.defaultAction) diff --git a/Sources/Aether/UI/MainWindow/ToolbarView.swift b/Sources/Aether/UI/MainWindow/ToolbarView.swift index bc254b9..4a7ff81 100644 --- a/Sources/Aether/UI/MainWindow/ToolbarView.swift +++ b/Sources/Aether/UI/MainWindow/ToolbarView.swift @@ -10,7 +10,7 @@ struct ToolbarView: View { HStack(spacing: 8) { ToolbarButton( icon: "doc.badge.plus", - label: "Open", + label: translate("toolbar.open"), shortcut: "O" ) { appState.openFile() @@ -18,7 +18,7 @@ struct ToolbarView: View { ToolbarButton( icon: "square.and.arrow.down", - label: "Save", + label: translate("toolbar.save"), shortcut: "S" ) { appState.saveFileAs() @@ -27,7 +27,7 @@ struct ToolbarView: View { ToolbarButton( icon: "arrow.clockwise", - label: "Reload", + label: translate("toolbar.reload"), shortcut: nil ) { if let url = appState.currentFile?.url { @@ -40,7 +40,7 @@ struct ToolbarView: View { ToolbarButton( icon: "xmark.circle", - label: "Close", + label: translate("toolbar.close"), shortcut: "W" ) { appState.closeFile() @@ -53,7 +53,7 @@ struct ToolbarView: View { Circle() .fill(Color.orange) .frame(width: 8, height: 8) - .help("Unsaved changes") + .help(translate("toolbar.unsavedChanges")) } Divider() @@ -63,7 +63,7 @@ struct ToolbarView: View { HStack(spacing: 8) { ToolbarButton( icon: "cpu", - label: "Analyze", + label: translate("toolbar.analyze"), shortcut: "A" ) { appState.analyzeAll() @@ -72,7 +72,7 @@ struct ToolbarView: View { ToolbarButton( icon: "function", - label: "Functions", + label: translate("toolbar.functions"), shortcut: nil ) { appState.findFunctions() @@ -87,19 +87,19 @@ struct ToolbarView: View { HStack(spacing: 8) { ToolbarToggle( icon: "rectangle.split.2x1", - label: "Decompiler", + label: translate("toolbar.decompiler"), isOn: $appState.showDecompiler ) ToolbarToggle( icon: "rectangle.bottomhalf.filled", - label: "Hex View", + label: translate("toolbar.hexView"), isOn: $appState.showHexView ) ToolbarButton( icon: "point.3.connected.trianglepath.dotted", - label: "CFG", + label: translate("toolbar.cfg"), shortcut: "G" ) { appState.showCFG.toggle() @@ -118,7 +118,7 @@ struct ToolbarView: View { Button { appState.showAIChat = true } label: { - Label("Chat with AI...", systemImage: "bubble.left.and.bubble.right") + Label(translate("toolbar.ai.chat"), systemImage: "bubble.left.and.bubble.right") } Divider() @@ -127,14 +127,14 @@ struct ToolbarView: View { Button { appState.explainCurrentFunction() } label: { - Label("Explain Function", systemImage: "text.bubble") + Label(translate("toolbar.ai.explainFunction"), systemImage: "text.bubble") } .disabled(appState.selectedFunction == nil) Button { appState.suggestVariableNames() } label: { - Label("Rename Variables", systemImage: "textformat.abc") + Label(translate("toolbar.ai.renameVariables"), systemImage: "textformat.abc") } .disabled(appState.selectedFunction == nil || appState.decompilerOutput.isEmpty) @@ -144,14 +144,14 @@ struct ToolbarView: View { Button { appState.analyzeWithAI() } label: { - Label("Security Analysis", systemImage: "shield.lefthalf.filled") + Label(translate("toolbar.ai.securityAnalysis"), systemImage: "shield.lefthalf.filled") } .disabled(appState.selectedFunction == nil) Button { appState.analyzeBinaryWithAI() } label: { - Label("Analyze Binary", systemImage: "doc.viewfinder") + Label(translate("toolbar.ai.analyzeBinary"), systemImage: "doc.viewfinder") } .disabled(appState.currentFile == nil) } label: { @@ -159,7 +159,7 @@ struct ToolbarView: View { Image(systemName: "brain") .font(.system(size: 16)) .foregroundColor(.purple) - Text("AI") + Text(translate("toolbar.ai")) .font(.caption2) } .frame(minWidth: 40) @@ -170,18 +170,18 @@ struct ToolbarView: View { } else { ToolbarButton( icon: "brain", - label: "AI", + label: translate("toolbar.ai"), shortcut: nil ) { openSettings() } .opacity(0.5) - .help("Configure API key in Settings") + .help(translate("toolbar.ai.configure")) } ToolbarButton( icon: "gear", - label: "Settings", + label: translate("toolbar.settings"), shortcut: "," ) { openSettings() @@ -196,7 +196,7 @@ struct ToolbarView: View { Button { appState.generateFridaScript() } label: { - Label("Generate Basic Script", systemImage: "doc.text") + Label(translate("toolbar.frida.generateBasic"), systemImage: "doc.text") } .disabled(appState.selectedFunction == nil) @@ -204,7 +204,7 @@ struct ToolbarView: View { Button { appState.generateFridaScriptWithAI() } label: { - Label("Generate with AI", systemImage: "brain") + Label(translate("toolbar.frida.generateWithAI"), systemImage: "brain") } .disabled(appState.selectedFunction == nil) } @@ -212,14 +212,14 @@ struct ToolbarView: View { Button { appState.generateMultiFunctionFridaScript() } label: { - Label("Hook Multiple Functions", systemImage: "list.bullet") + Label(translate("toolbar.frida.hookMultiple"), systemImage: "list.bullet") } .disabled(appState.functions.isEmpty) Divider() // Platform submenu - Menu("Platform") { + Menu(translate("toolbar.frida.platform")) { ForEach(FridaPlatform.allCases) { platform in Button { appState.selectedFridaPlatform = platform @@ -235,7 +235,7 @@ struct ToolbarView: View { } // Hook type submenu - Menu("Hook Type") { + Menu(translate("toolbar.frida.hookType")) { ForEach(FridaHookType.allCases) { type in Button { appState.selectedFridaHookType = type @@ -254,7 +254,7 @@ struct ToolbarView: View { Image(systemName: "hammer.fill") .font(.system(size: 16)) .foregroundColor(.orange) - Text("Frida") + Text(translate("toolbar.frida")) .font(.caption2) } .frame(minWidth: 50) @@ -271,7 +271,7 @@ struct ToolbarView: View { Image(systemName: "magnifyingglass") .foregroundColor(.secondary) - Text("Search...") + Text(translate("toolbar.search")) .foregroundColor(.secondary) } .padding(.horizontal, 12) @@ -285,7 +285,7 @@ struct ToolbarView: View { // Quick navigation ToolbarButton( icon: "arrow.right.circle", - label: "Go to", + label: translate("toolbar.goto"), shortcut: "G" ) { appState.showGoToAddress = true @@ -332,7 +332,7 @@ struct ToolbarButton: View { .onHover { hovering in isHovered = hovering } - .help(shortcut != nil ? "\(label) (\(shortcut!))" : label) + .help(shortcut ?? "") } } @@ -367,6 +367,6 @@ struct ToolbarToggle: View { .onHover { hovering in isHovered = hovering } - .help(label) + .help("") } } diff --git a/Sources/Aether/UI/Settings/SettingsView.swift b/Sources/Aether/UI/Settings/SettingsView.swift index f57ea19..5f3655c 100644 --- a/Sources/Aether/UI/Settings/SettingsView.swift +++ b/Sources/Aether/UI/Settings/SettingsView.swift @@ -22,15 +22,15 @@ struct AISettingsTab: View { .font(.title2) .foregroundColor(.purple) VStack(alignment: .leading) { - Text("AI Security Analysis") + Text(translate("settings.ai.title")) .font(.headline) - Text("Analyze code for vulnerabilities with AI") + Text(translate("settings.ai.subtitle")) .font(.caption) .foregroundColor(.secondary) } Spacer() if apiKeyConfigured { - Label("Active", systemImage: "checkmark.circle.fill") + Label(translate("settings.ai.active"), systemImage: "checkmark.circle.fill") .foregroundColor(.green) .font(.caption) } @@ -40,17 +40,17 @@ struct AISettingsTab: View { // API Key Section VStack(alignment: .leading, spacing: 8) { - Text("API Key") + Text(translate("settings.ai.apiKey")) .font(.subheadline) .fontWeight(.medium) HStack { if showAPIKey { - TextField("sk-ant-...", text: $apiKey) + TextField(translate("settings.ai.apiKeyPlaceholder"), text: $apiKey) .textFieldStyle(.roundedBorder) .font(.system(.body, design: .monospaced)) } else { - SecureField("sk-ant-...", text: $apiKey) + SecureField(translate("settings.ai.apiKeyPlaceholder"), text: $apiKey) .textFieldStyle(.roundedBorder) .font(.system(.body, design: .monospaced)) } @@ -63,21 +63,21 @@ struct AISettingsTab: View { .buttonStyle(.plain) } - Text("Get your API key from console.anthropic.com") + Text(translate("settings.ai.apiKeyHelp")) .font(.caption) .foregroundColor(.secondary) } // Actions HStack { - Button("Save Key") { + Button(translate("settings.ai.saveKey")) { saveAPIKey() } .buttonStyle(.borderedProminent) .disabled(apiKey.isEmpty || isValidating) if apiKeyConfigured { - Button("Remove") { + Button(translate("settings.ai.remove")) { removeAPIKey() } .foregroundColor(.red) @@ -92,7 +92,7 @@ struct AISettingsTab: View { switch saveStatus { case .success: - Label("Saved!", systemImage: "checkmark.circle") + Label(translate("settings.ai.saved"), systemImage: "checkmark.circle") .foregroundColor(.green) .font(.caption) case .error(let message): @@ -108,11 +108,11 @@ struct AISettingsTab: View { // Info VStack(alignment: .leading, spacing: 6) { - Label("Analyzes code for security vulnerabilities", systemImage: "shield.lefthalf.filled") + Label(translate("settings.ai.info.analyzes"), systemImage: "shield.lefthalf.filled") .font(.caption) - Label("API key stored in macOS Keychain", systemImage: "lock.shield") + Label(translate("settings.ai.info.keychain"), systemImage: "lock.shield") .font(.caption) - Label("Uses your API credits", systemImage: "dollarsign.circle") + Label(translate("settings.ai.info.credits"), systemImage: "dollarsign.circle") .font(.caption) } .foregroundColor(.secondary) @@ -133,7 +133,7 @@ struct AISettingsTab: View { // Validate API key format guard apiKey.hasPrefix("sk-ant-") else { - saveStatus = .error("Invalid format") + saveStatus = .error(translate("settings.ai.error.invalidFormat")) isValidating = false return } @@ -150,7 +150,7 @@ struct AISettingsTab: View { apiKey = String(repeating: "*", count: 20) } } else { - saveStatus = .error("Save failed") + saveStatus = .error(translate("settings.ai.error.saveFailed")) } } diff --git a/scripts/build_dmg.sh b/scripts/build_dmg.sh new file mode 100755 index 0000000..8f5b52e --- /dev/null +++ b/scripts/build_dmg.sh @@ -0,0 +1,465 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP_NAME="Aether" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +DIST_DIR="${ROOT_DIR}/dist" +WORK_DIR="${ROOT_DIR}/.build/release-dmg" +DMG_STAGE_DIR="${WORK_DIR}/dmg-stage" +PROJECT_RESOURCES_DIR="${ROOT_DIR}/Sources/Aether/Resources" + +APP_SIGN_IDENTITY="${APP_SIGN_IDENTITY:-}" +NOTARY_PROFILE="${NOTARY_PROFILE:-}" +SKIP_NOTARIZATION="${SKIP_NOTARIZATION:-0}" +TARGET_ARCH="${TARGET_ARCH:-universal}" +BUNDLE_ID="${BUNDLE_ID:-com.aether.app}" +APP_VERSION="${APP_VERSION:-}" +APP_BUILD="${APP_BUILD:-}" +BUILD_TIMESTAMP="${BUILD_TIMESTAMP:-}" +BUILD_COMMIT="${BUILD_COMMIT:-}" +LICENSE_NAME="${LICENSE_NAME:-MIT License}" +MIN_MACOS_VERSION="${MIN_MACOS_VERSION:-14.0}" +DMG_VOLUME_NAME="${DMG_VOLUME_NAME:-Aether}" +APP_ONLY="${APP_ONLY:-0}" +OPEN_APP="${OPEN_APP:-0}" + +usage() { + cat <.dmg. + +Options: + --version App version (default: latest git tag or 0.0.0) + --arch universal (default), arm64, x86_64 + --bundle-id Bundle identifier (default: com.aether.app) + --app-only Build/sign app bundle only (no DMG/notarization) + --open-app Open resulting app bundle when done + --skip-notarization Skip notarization/stapling + -h, --help Show this help + +Required env vars (unless --app-only is used): + APP_SIGN_IDENTITY Developer ID Application identity for codesign + +Required unless --skip-notarization is used: + NOTARY_PROFILE Keychain profile configured via xcrun notarytool store-credentials + +Optional env vars: + APP_VERSION Same as --version + APP_BUILD CFBundleVersion (default: git short hash, fallback APP_VERSION) + BUILD_TIMESTAMP ISO8601 UTC timestamp (default: current UTC time) + BUILD_COMMIT Source revision marker (default: git short hash) + LICENSE_NAME License label shown in About window (default: MIT License) + TARGET_ARCH Same as --arch + BUNDLE_ID Same as --bundle-id + MIN_MACOS_VERSION Defaults to 14.0 + DMG_VOLUME_NAME Defaults to Aether +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --version) + APP_VERSION="$2" + shift 2 + ;; + --arch) + TARGET_ARCH="$2" + shift 2 + ;; + --bundle-id) + BUNDLE_ID="$2" + shift 2 + ;; + --app-only) + APP_ONLY=1 + shift + ;; + --open-app) + OPEN_APP=1 + shift + ;; + --skip-notarization) + SKIP_NOTARIZATION=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "Missing required command: $1" >&2 + exit 1 + } +} + +generate_app_icon_icns() { + local app_iconset_src="$1" + local app_resources_dir="$2" + local iconset_dir="${WORK_DIR}/AppIcon.iconset" + local base_png="${app_iconset_src}/icon_1024.png" + + if [[ ! -d "${app_iconset_src}" ]]; then + echo "Warning: App iconset not found at ${app_iconset_src}" >&2 + return 0 + fi + + if [[ ! -f "${base_png}" ]]; then + base_png="$(find "${app_iconset_src}" -maxdepth 1 -type f -name '*.png' -print | head -n 1 || true)" + fi + + if [[ -z "${base_png}" || ! -f "${base_png}" ]]; then + echo "Warning: no PNG files found in ${app_iconset_src}" >&2 + return 0 + fi + + rm -rf "${iconset_dir}" + mkdir -p "${iconset_dir}" + + sips -z 16 16 "${base_png}" --out "${iconset_dir}/icon_16x16.png" >/dev/null + sips -z 32 32 "${base_png}" --out "${iconset_dir}/icon_16x16@2x.png" >/dev/null + sips -z 32 32 "${base_png}" --out "${iconset_dir}/icon_32x32.png" >/dev/null + sips -z 64 64 "${base_png}" --out "${iconset_dir}/icon_32x32@2x.png" >/dev/null + sips -z 128 128 "${base_png}" --out "${iconset_dir}/icon_128x128.png" >/dev/null + sips -z 256 256 "${base_png}" --out "${iconset_dir}/icon_128x128@2x.png" >/dev/null + sips -z 256 256 "${base_png}" --out "${iconset_dir}/icon_256x256.png" >/dev/null + sips -z 512 512 "${base_png}" --out "${iconset_dir}/icon_256x256@2x.png" >/dev/null + sips -z 512 512 "${base_png}" --out "${iconset_dir}/icon_512x512.png" >/dev/null + cp "${base_png}" "${iconset_dir}/icon_512x512@2x.png" + + iconutil -c icns "${iconset_dir}" -o "${app_resources_dir}/AppIcon.icns" +} + +log() { + printf '\n[%s] %s\n' "$(date +'%H:%M:%S')" "$*" +} + +resolve_release_dir() { + local arch="$1" + local path + + path="$(find "${ROOT_DIR}/.build" -type d -path "*/${arch}-apple-macosx*/release" -print | head -n 1 || true)" + if [[ -z "${path}" ]]; then + echo "Could not locate release directory for architecture: ${arch}" >&2 + return 1 + fi + + printf '%s\n' "${path}" +} + +if [[ "${APP_ONLY}" == "1" ]]; then + SKIP_NOTARIZATION=1 +fi + +if [[ -z "${APP_SIGN_IDENTITY}" ]]; then + if [[ "${APP_ONLY}" == "1" ]]; then + APP_SIGN_IDENTITY="-" + else + echo "APP_SIGN_IDENTITY is required." >&2 + exit 1 + fi +fi + +if [[ "${TARGET_ARCH}" != "universal" && "${TARGET_ARCH}" != "arm64" && "${TARGET_ARCH}" != "x86_64" ]]; then + echo "Invalid --arch value: ${TARGET_ARCH}. Expected one of: universal, arm64, x86_64" >&2 + exit 1 +fi + +if [[ "${SKIP_NOTARIZATION}" != "1" && -z "${NOTARY_PROFILE}" ]]; then + echo "NOTARY_PROFILE is required unless --skip-notarization is used." >&2 + exit 1 +fi + +if [[ -z "${APP_VERSION}" ]]; then + APP_VERSION="$(git -C "${ROOT_DIR}" describe --tags --abbrev=0 2>/dev/null || true)" + APP_VERSION="${APP_VERSION#v}" +fi + +if [[ -z "${APP_VERSION}" ]]; then + APP_VERSION="0.0.0" +fi + +if [[ -z "${APP_BUILD}" ]]; then + APP_BUILD="$(git -C "${ROOT_DIR}" rev-parse --short=8 HEAD 2>/dev/null || true)" +fi + +if [[ -z "${APP_BUILD}" ]]; then + APP_BUILD="${APP_VERSION}" +fi + +if [[ -z "${BUILD_TIMESTAMP}" ]]; then + BUILD_TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +fi + +if [[ -z "${BUILD_COMMIT}" ]]; then + BUILD_COMMIT="$(git -C "${ROOT_DIR}" rev-parse --short=12 HEAD 2>/dev/null || true)" +fi + +if [[ -z "${BUILD_COMMIT}" ]]; then + BUILD_COMMIT="${APP_BUILD}" +fi + +require_cmd swift +require_cmd lipo +require_cmd codesign +require_cmd sips +require_cmd iconutil +if [[ "${APP_ONLY}" != "1" ]]; then + require_cmd hdiutil +fi +if [[ "${SKIP_NOTARIZATION}" != "1" ]]; then + require_cmd ditto + require_cmd xcrun +fi +if [[ "${OPEN_APP}" == "1" ]]; then + require_cmd open +fi + +log "Cleaning previous artifacts" +rm -rf "${WORK_DIR}" "${DIST_DIR}" +mkdir -p "${WORK_DIR}" "${DIST_DIR}" + +log "Build metadata: version=${APP_VERSION}, build=${APP_BUILD}, commit=${BUILD_COMMIT}, arch=${TARGET_ARCH}" + +APP_DIR="${WORK_DIR}/${APP_NAME}.app" +mkdir -p "${APP_DIR}/Contents/MacOS" "${APP_DIR}/Contents/Resources" + +RESOURCE_RELEASE_DIR="" +if [[ "${TARGET_ARCH}" == "universal" ]]; then + log "Building release binaries (arm64 + x86_64)" + swift build --package-path "${ROOT_DIR}" -c release --arch arm64 + swift build --package-path "${ROOT_DIR}" -c release --arch x86_64 + + ARM_RELEASE_DIR="$(resolve_release_dir arm64)" + X86_RELEASE_DIR="$(resolve_release_dir x86_64)" + ARM_BINARY="${ARM_RELEASE_DIR}/${APP_NAME}" + X86_BINARY="${X86_RELEASE_DIR}/${APP_NAME}" + + if [[ ! -f "${ARM_BINARY}" ]]; then + echo "Missing arm64 binary: ${ARM_BINARY}" >&2 + exit 1 + fi + + if [[ ! -f "${X86_BINARY}" ]]; then + echo "Missing x86_64 binary: ${X86_BINARY}" >&2 + exit 1 + fi + + log "Creating universal executable" + lipo -create "${ARM_BINARY}" "${X86_BINARY}" -output "${APP_DIR}/Contents/MacOS/${APP_NAME}" + RESOURCE_RELEASE_DIR="${ARM_RELEASE_DIR}" +else + log "Building release binary (${TARGET_ARCH})" + swift build --package-path "${ROOT_DIR}" -c release --arch "${TARGET_ARCH}" + RELEASE_DIR="$(resolve_release_dir "${TARGET_ARCH}")" + ARCH_BINARY="${RELEASE_DIR}/${APP_NAME}" + + if [[ ! -f "${ARCH_BINARY}" ]]; then + echo "Missing ${TARGET_ARCH} binary: ${ARCH_BINARY}" >&2 + exit 1 + fi + + cp "${ARCH_BINARY}" "${APP_DIR}/Contents/MacOS/${APP_NAME}" + RESOURCE_RELEASE_DIR="${RELEASE_DIR}" +fi + +chmod 755 "${APP_DIR}/Contents/MacOS/${APP_NAME}" + +log "Copying SwiftPM resource bundles" +found_bundle=0 +while IFS= read -r -d '' bundle_path; do + cp -R "${bundle_path}" "${APP_DIR}/Contents/Resources/" + found_bundle=1 +done < <(find "${RESOURCE_RELEASE_DIR}" -maxdepth 1 -type d -name "*.bundle" -print0) + +if [[ "${found_bundle}" -eq 0 ]]; then + echo "Warning: no .bundle resources found in ${RESOURCE_RELEASE_DIR}" >&2 +fi + +if [[ -d "${PROJECT_RESOURCES_DIR}" ]]; then + log "Copying source resources from ${PROJECT_RESOURCES_DIR}" + mkdir -p "${APP_DIR}/Contents/Resources/AetherResources" + cp -R "${PROJECT_RESOURCES_DIR}/." "${APP_DIR}/Contents/Resources/AetherResources/" +fi + +if [[ -f "${ROOT_DIR}/LICENSE" ]]; then + cp "${ROOT_DIR}/LICENSE" "${APP_DIR}/Contents/Resources/LICENSE.txt" +fi + +log "Generating AppIcon.icns from source iconset (if available)" +generate_app_icon_icns \ + "${PROJECT_RESOURCES_DIR}/Assets.xcassets/AppIcon.appiconset" \ + "${APP_DIR}/Contents/Resources" + +log "Writing Info.plist" +cat > "${APP_DIR}/Contents/Info.plist" < + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${APP_NAME} + CFBundleIdentifier + ${BUNDLE_ID} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${APP_NAME} + CFBundlePackageType + APPL + CFBundleIconFile + AppIcon + CFBundleShortVersionString + ${APP_VERSION} + CFBundleVersion + ${APP_BUILD} + AetherBuildTimestamp + ${BUILD_TIMESTAMP} + AetherBuildTargetArch + ${TARGET_ARCH} + AetherBuildCommit + ${BUILD_COMMIT} + AetherLicense + ${LICENSE_NAME} + LSMinimumSystemVersion + ${MIN_MACOS_VERSION} + NSHighResolutionCapable + + NSPrincipalClass + NSApplication + + +PLIST + +log "Signing app bundle" +if [[ "${APP_ONLY}" == "1" ]]; then + # Local run path: avoid timestamp/runtime requirements that can block on keychain/network. + codesign --force --sign "${APP_SIGN_IDENTITY}" "${APP_DIR}" +else + codesign --force --timestamp --options runtime --sign "${APP_SIGN_IDENTITY}" "${APP_DIR}" +fi +codesign --verify --deep --strict --verbose=2 "${APP_DIR}" + +if [[ "${SKIP_NOTARIZATION}" != "1" ]]; then + APP_ZIP="${WORK_DIR}/${APP_NAME}.zip" + log "Creating zip for app notarization" + ditto -c -k --keepParent "${APP_DIR}" "${APP_ZIP}" + + log "Submitting app for notarization" + xcrun notarytool submit "${APP_ZIP}" --keychain-profile "${NOTARY_PROFILE}" --wait + + log "Stapling notarization ticket to app" + xcrun stapler staple "${APP_DIR}" + xcrun stapler validate "${APP_DIR}" +fi + +FINAL_APP="${DIST_DIR}/${APP_NAME}-${TARGET_ARCH}.app" +rm -rf "${FINAL_APP}" +cp -R "${APP_DIR}" "${FINAL_APP}" + +APP_EXEC_SHA256="$(shasum -a 256 "${APP_DIR}/Contents/MacOS/${APP_NAME}" | awk '{print $1}')" +APP_SHA_FILE="${DIST_DIR}/${APP_NAME}-${TARGET_ARCH}.app-executable.sha256" +printf '%s %s\n' "${APP_EXEC_SHA256}" "${APP_NAME}.app/Contents/MacOS/${APP_NAME}" > "${APP_SHA_FILE}" + +if [[ "${APP_ONLY}" == "1" ]]; then + MANIFEST_FILE="${DIST_DIR}/${APP_NAME}-${TARGET_ARCH}.build-manifest.json" + cat > "${MANIFEST_FILE}" < "${APP_SHA_FILE}" +printf '%s %s\n' "${DMG_SHA256}" "$(basename "${FINAL_DMG}")" > "${DMG_SHA_FILE}" + +cat > "${MANIFEST_FILE}" <] + +Builds Aether.app as a bundle (no DMG), then launches it. + +Options: + --arch arm64, x86_64, or universal + --sign-identity Override signing identity for local run (default: -) + -h, --help Show this help + +Examples: + scripts/run_app_bundle.sh + scripts/run_app_bundle.sh --arch universal + scripts/run_app_bundle.sh --sign-identity "Developer ID Application: Your Name (TEAMID)" + scripts/run_app_bundle.sh -- --version 1.2.1 +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --arch) + TARGET_ARCH="$2" + shift 2 + ;; + --sign-identity) + SIGN_IDENTITY="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + EXTRA_ARGS+=("$@") + break + ;; + *) + EXTRA_ARGS+=("$1") + shift + ;; + esac +done + +if [[ -z "${TARGET_ARCH}" ]]; then + host_arch="$(uname -m)" + case "${host_arch}" in + arm64|x86_64) + TARGET_ARCH="${host_arch}" + ;; + *) + TARGET_ARCH="universal" + ;; + esac +fi + +cd "${ROOT_DIR}" +APP_SIGN_IDENTITY="${SIGN_IDENTITY}" \ + "${SCRIPT_DIR}/build_dmg.sh" \ + --app-only \ + --open-app \ + --arch "${TARGET_ARCH}" \ + "${EXTRA_ARGS[@]}"