From 512208e1c402178be0f1079ec1fe099e8cee8412 Mon Sep 17 00:00:00 2001 From: Roberto Nibali Date: Sun, 15 Feb 2026 11:51:48 +0100 Subject: [PATCH 01/11] Add `.run` to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1c2162e..97806c7 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ xcuserdata/ *.swp *.swo *~ +.run # Temporary files *.tmp From 86d20f0ff5c953a71fb67629386c9dc467864cd0 Mon Sep 17 00:00:00 2001 From: Roberto Nibali Date: Sun, 15 Feb 2026 12:16:19 +0100 Subject: [PATCH 02/11] Add localization and AppLanguage support --- Package.swift | 1 + Sources/Aether/App/AetherApp.swift | 113 +++++++++++------- Sources/Aether/App/AppLanguage.swift | 109 +++++++++++++++++ Sources/Aether/App/Translate.swift | 30 +++++ .../Resources/de.lproj/Localizable.strings | 107 +++++++++++++++++ .../Resources/en.lproj/Localizable.strings | 107 +++++++++++++++++ .../Resources/es.lproj/Localizable.strings | 107 +++++++++++++++++ .../Resources/fr.lproj/Localizable.strings | 107 +++++++++++++++++ .../Resources/it.lproj/Localizable.strings | 107 +++++++++++++++++ Sources/Aether/UI/MainWindow/MainView.swift | 18 +-- .../Aether/UI/MainWindow/ToolbarView.swift | 58 ++++----- Sources/Aether/UI/Settings/SettingsView.swift | 30 ++--- 12 files changed, 797 insertions(+), 97 deletions(-) create mode 100644 Sources/Aether/App/AppLanguage.swift create mode 100644 Sources/Aether/App/Translate.swift create mode 100644 Sources/Aether/Resources/de.lproj/Localizable.strings create mode 100644 Sources/Aether/Resources/en.lproj/Localizable.strings create mode 100644 Sources/Aether/Resources/es.lproj/Localizable.strings create mode 100644 Sources/Aether/Resources/fr.lproj/Localizable.strings create mode 100644 Sources/Aether/Resources/it.lproj/Localizable.strings 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..a5b1819 100644 --- a/Sources/Aether/App/AetherApp.swift +++ b/Sources/Aether/App/AetherApp.swift @@ -4,35 +4,38 @@ 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: .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 +43,7 @@ struct AetherApp: App { Divider() - Button("Close") { + Button(translate("menu.file.close")) { appState.closeFile() } .keyboardShortcut("w", modifiers: .command) @@ -48,26 +51,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 +78,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 +98,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 +196,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 +220,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 +245,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.displayName).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 +272,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 +280,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 +303,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() } diff --git a/Sources/Aether/App/AppLanguage.swift b/Sources/Aether/App/AppLanguage.swift new file mode 100644 index 0000000..7d29930 --- /dev/null +++ b/Sources/Aether/App/AppLanguage.swift @@ -0,0 +1,109 @@ +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) + } + + 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) + } +} 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..c66c63c --- /dev/null +++ b/Sources/Aether/Resources/de.lproj/Localizable.strings @@ -0,0 +1,107 @@ +"common.error" = "Fehler"; +"common.ok" = "OK"; +"common.cancel" = "Abbrechen"; +"common.go" = "Los"; +"common.unknownError" = "Unbekannter Fehler"; + +"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..2c2eb46 --- /dev/null +++ b/Sources/Aether/Resources/en.lproj/Localizable.strings @@ -0,0 +1,107 @@ +"common.error" = "Error"; +"common.ok" = "OK"; +"common.cancel" = "Cancel"; +"common.go" = "Go"; +"common.unknownError" = "Unknown error"; + +"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..419fa98 --- /dev/null +++ b/Sources/Aether/Resources/es.lproj/Localizable.strings @@ -0,0 +1,107 @@ +"common.error" = "Error"; +"common.ok" = "OK"; +"common.cancel" = "Cancelar"; +"common.go" = "Ir"; +"common.unknownError" = "Error desconocido"; + +"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..cad5d0c --- /dev/null +++ b/Sources/Aether/Resources/fr.lproj/Localizable.strings @@ -0,0 +1,107 @@ +"common.error" = "Erreur"; +"common.ok" = "OK"; +"common.cancel" = "Annuler"; +"common.go" = "Aller"; +"common.unknownError" = "Erreur inconnue"; + +"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..da8a82c --- /dev/null +++ b/Sources/Aether/Resources/it.lproj/Localizable.strings @@ -0,0 +1,107 @@ +"common.error" = "Errore"; +"common.ok" = "OK"; +"common.cancel" = "Annulla"; +"common.go" = "Vai"; +"common.unknownError" = "Errore sconosciuto"; + +"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/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")) } } From f45ba1f98a19f41beeb7ffca0d7498e0bce7efdb Mon Sep 17 00:00:00 2001 From: Roberto Nibali Date: Sun, 15 Feb 2026 12:26:11 +0100 Subject: [PATCH 03/11] Add flag emoji support to AppLanguage picker labels --- Sources/Aether/App/AetherApp.swift | 2 +- Sources/Aether/App/AppLanguage.swift | 44 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/Sources/Aether/App/AetherApp.swift b/Sources/Aether/App/AetherApp.swift index a5b1819..078f4ae 100644 --- a/Sources/Aether/App/AetherApp.swift +++ b/Sources/Aether/App/AetherApp.swift @@ -251,7 +251,7 @@ struct GeneralSettingsView: View { Form { Picker(translate("settings.general.language"), selection: $appLanguage) { ForEach(AppLanguage.available) { language in - Text(language.displayName).tag(language.code) + Text(language.pickerLabel).tag(language.code) } } Toggle(translate("settings.general.autoAnalyze"), isOn: $autoAnalyze) diff --git a/Sources/Aether/App/AppLanguage.swift b/Sources/Aether/App/AppLanguage.swift index 7d29930..348503f 100644 --- a/Sources/Aether/App/AppLanguage.swift +++ b/Sources/Aether/App/AppLanguage.swift @@ -24,6 +24,18 @@ struct AppLanguage: Identifiable, Hashable { 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) } @@ -106,4 +118,36 @@ struct AppLanguage: Identifiable, Hashable { 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) + } } From 3208fe0afcb6eed92f50ce74327e86f9cfc3efa8 Mon Sep 17 00:00:00 2001 From: Roberto Nibali Date: Sun, 15 Feb 2026 12:59:10 +0100 Subject: [PATCH 04/11] Add `build_dmg.sh` script for macOS DMG creation, signing, and notarization, with accompanying `BUILD.md` documentation --- BUILD.md | 160 +++++++++++++++++++++++ scripts/build_dmg.sh | 298 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 458 insertions(+) create mode 100644 BUILD.md create mode 100755 scripts/build_dmg.sh diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..fcb0098 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,160 @@ +# Build and Release (Signed + Notarized DMG) + +This project includes `scripts/build_dmg.sh` to produce a distributable macOS DMG: + +- Output: `dist/Aether.dmg` +- Includes a universal binary (`arm64` + `x86_64`) +- Signs the `.app` and `.dmg` +- Notarizes and staples both (unless `--skip-notarization`) + +## 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" +``` + +If `--version` is omitted, the script uses the latest git tag (without leading `v`) or falls back to `0.0.0`. + +## Useful script options + +```bash +scripts/build_dmg.sh --help +``` + +- `--bundle-id `: CFBundleIdentifier in `Info.plist` +- `--version `: app short/build version +- `--skip-notarization`: sign only, skip notary submission/stapling + +## 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. + +## Verify output + +After build: + +```bash +ls -lh dist/Aether.dmg +spctl -a -vvv -t open dist/Aether.dmg +``` + +You can also mount and inspect: + +```bash +hdiutil attach dist/Aether.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/scripts/build_dmg.sh b/scripts/build_dmg.sh new file mode 100755 index 0000000..707aa9b --- /dev/null +++ b/scripts/build_dmg.sh @@ -0,0 +1,298 @@ +#!/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}" +BUNDLE_ID="${BUNDLE_ID:-com.aether.app}" +APP_VERSION="${APP_VERSION:-}" +MIN_MACOS_VERSION="${MIN_MACOS_VERSION:-14.0}" +DMG_VOLUME_NAME="${DMG_VOLUME_NAME:-Aether}" + +usage() { + cat < App version (default: latest git tag or 0.0.0) + --bundle-id Bundle identifier (default: com.aether.app) + --skip-notarization Skip notarization/stapling + -h, --help Show this help + +Required env vars: + 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 + 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 + ;; + --bundle-id) + BUNDLE_ID="$2" + shift 2 + ;; + --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 [[ -z "${APP_SIGN_IDENTITY}" ]]; then + echo "APP_SIGN_IDENTITY is required." >&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 + +require_cmd swift +require_cmd lipo +require_cmd codesign +require_cmd hdiutil +require_cmd ditto +require_cmd xcrun +require_cmd sips +require_cmd iconutil + +log "Cleaning previous artifacts" +rm -rf "${WORK_DIR}" "${DIST_DIR}" +mkdir -p "${WORK_DIR}" "${DIST_DIR}" + +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 + +APP_DIR="${WORK_DIR}/${APP_NAME}.app" +mkdir -p "${APP_DIR}/Contents/MacOS" "${APP_DIR}/Contents/Resources" + +log "Creating universal executable" +lipo -create "${ARM_BINARY}" "${X86_BINARY}" -output "${APP_DIR}/Contents/MacOS/${APP_NAME}" +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 "${ARM_RELEASE_DIR}" -maxdepth 1 -type d -name "*.bundle" -print0) + +if [[ "${found_bundle}" -eq 0 ]]; then + echo "Warning: no .bundle resources found in ${ARM_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 + +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_VERSION} + LSMinimumSystemVersion + ${MIN_MACOS_VERSION} + NSHighResolutionCapable + + NSPrincipalClass + NSApplication + + +PLIST + +log "Signing app bundle" +codesign --force --timestamp --options runtime --sign "${APP_SIGN_IDENTITY}" "${APP_DIR}" +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 + +log "Preparing DMG staging folder" +rm -rf "${DMG_STAGE_DIR}" +mkdir -p "${DMG_STAGE_DIR}" +cp -R "${APP_DIR}" "${DMG_STAGE_DIR}/" +ln -s /Applications "${DMG_STAGE_DIR}/Applications" + +UNSIGNED_DMG="${WORK_DIR}/${APP_NAME}.dmg" +FINAL_DMG="${DIST_DIR}/${APP_NAME}.dmg" + +log "Building DMG" +hdiutil create \ + -volname "${DMG_VOLUME_NAME}" \ + -srcfolder "${DMG_STAGE_DIR}" \ + -format UDZO \ + -fs HFS+ \ + "${UNSIGNED_DMG}" + +log "Signing DMG" +codesign --force --timestamp --sign "${APP_SIGN_IDENTITY}" "${UNSIGNED_DMG}" +codesign --verify --verbose=2 "${UNSIGNED_DMG}" + +if [[ "${SKIP_NOTARIZATION}" != "1" ]]; then + log "Submitting DMG for notarization" + xcrun notarytool submit "${UNSIGNED_DMG}" --keychain-profile "${NOTARY_PROFILE}" --wait + + log "Stapling notarization ticket to DMG" + xcrun stapler staple "${UNSIGNED_DMG}" + xcrun stapler validate "${UNSIGNED_DMG}" +fi + +mv -f "${UNSIGNED_DMG}" "${FINAL_DMG}" + +log "Done" +echo "DMG: ${FINAL_DMG}" +shasum -a 256 "${FINAL_DMG}" From e55c4e53ee50ce2e6d350f84c91888bad16cafae Mon Sep 17 00:00:00 2001 From: Roberto Nibali Date: Sun, 15 Feb 2026 13:07:23 +0100 Subject: [PATCH 05/11] Add architecture-specific builds to `build_dmg.sh` with `--arch` option Enhance `build_dmg.sh` to support building architecture-specific binaries (`universal`, `arm64`, `x86_64`) using the new `--arch` option. Update `BUILD.md` to reflect changes and introduce `APP_BUILD` for `CFBundleVersion`. --- BUILD.md | 25 +++++++++--- scripts/build_dmg.sh | 90 ++++++++++++++++++++++++++++++++------------ 2 files changed, 85 insertions(+), 30 deletions(-) diff --git a/BUILD.md b/BUILD.md index fcb0098..6eff234 100644 --- a/BUILD.md +++ b/BUILD.md @@ -2,8 +2,8 @@ This project includes `scripts/build_dmg.sh` to produce a distributable macOS DMG: -- Output: `dist/Aether.dmg` -- Includes a universal binary (`arm64` + `x86_64`) +- 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`) @@ -100,11 +100,18 @@ 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" +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) + +So About shows `Version X.Y.Z ()` instead of duplicating the same value twice. + ## Useful script options ```bash @@ -113,8 +120,14 @@ scripts/build_dmg.sh --help - `--bundle-id `: CFBundleIdentifier in `Info.plist` - `--version `: app short/build version +- `--arch `: `universal` (default), `arm64`, or `x86_64` - `--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` + ## Resource handling in the DMG build `scripts/build_dmg.sh` includes resources from both: @@ -145,14 +158,14 @@ This uses ad-hoc signing and is not suitable for public distribution. After build: ```bash -ls -lh dist/Aether.dmg -spctl -a -vvv -t open dist/Aether.dmg +ls -lh dist/Aether-*.dmg +spctl -a -vvv -t open dist/Aether-universal.dmg ``` You can also mount and inspect: ```bash -hdiutil attach dist/Aether.dmg +hdiutil attach dist/Aether-universal.dmg ``` ## CI note diff --git a/scripts/build_dmg.sh b/scripts/build_dmg.sh index 707aa9b..9302b16 100755 --- a/scripts/build_dmg.sh +++ b/scripts/build_dmg.sh @@ -12,8 +12,10 @@ 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:-}" MIN_MACOS_VERSION="${MIN_MACOS_VERSION:-14.0}" DMG_VOLUME_NAME="${DMG_VOLUME_NAME:-Aether}" @@ -21,10 +23,11 @@ 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) --skip-notarization Skip notarization/stapling -h, --help Show this help @@ -37,6 +40,8 @@ Required unless --skip-notarization is used: Optional env vars: APP_VERSION Same as --version + APP_BUILD CFBundleVersion (default: git short hash, fallback APP_VERSION) + TARGET_ARCH Same as --arch BUNDLE_ID Same as --bundle-id MIN_MACOS_VERSION Defaults to 14.0 DMG_VOLUME_NAME Defaults to Aether @@ -49,6 +54,10 @@ while [[ $# -gt 0 ]]; do APP_VERSION="$2" shift 2 ;; + --arch) + TARGET_ARCH="$2" + shift 2 + ;; --bundle-id) BUNDLE_ID="$2" shift 2 @@ -135,6 +144,11 @@ if [[ -z "${APP_SIGN_IDENTITY}" ]]; then exit 1 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 @@ -149,6 +163,14 @@ 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 + require_cmd swift require_cmd lipo require_cmd codesign @@ -162,30 +184,50 @@ log "Cleaning previous artifacts" rm -rf "${WORK_DIR}" "${DIST_DIR}" mkdir -p "${WORK_DIR}" "${DIST_DIR}" -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 +log "Build metadata: version=${APP_VERSION}, build=${APP_BUILD}, arch=${TARGET_ARCH}" -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}" +APP_DIR="${WORK_DIR}/${APP_NAME}.app" +mkdir -p "${APP_DIR}/Contents/MacOS" "${APP_DIR}/Contents/Resources" -if [[ ! -f "${ARM_BINARY}" ]]; then - echo "Missing arm64 binary: ${ARM_BINARY}" >&2 - exit 1 -fi +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 -if [[ ! -f "${X86_BINARY}" ]]; then - echo "Missing x86_64 binary: ${X86_BINARY}" >&2 - exit 1 -fi + 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}" -APP_DIR="${WORK_DIR}/${APP_NAME}.app" -mkdir -p "${APP_DIR}/Contents/MacOS" "${APP_DIR}/Contents/Resources" + 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 -log "Creating universal executable" -lipo -create "${ARM_BINARY}" "${X86_BINARY}" -output "${APP_DIR}/Contents/MacOS/${APP_NAME}" chmod 755 "${APP_DIR}/Contents/MacOS/${APP_NAME}" log "Copying SwiftPM resource bundles" @@ -193,10 +235,10 @@ found_bundle=0 while IFS= read -r -d '' bundle_path; do cp -R "${bundle_path}" "${APP_DIR}/Contents/Resources/" found_bundle=1 -done < <(find "${ARM_RELEASE_DIR}" -maxdepth 1 -type d -name "*.bundle" -print0) +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 ${ARM_RELEASE_DIR}" >&2 + echo "Warning: no .bundle resources found in ${RESOURCE_RELEASE_DIR}" >&2 fi if [[ -d "${PROJECT_RESOURCES_DIR}" ]]; then @@ -233,7 +275,7 @@ cat > "${APP_DIR}/Contents/Info.plist" <CFBundleShortVersionString ${APP_VERSION} CFBundleVersion - ${APP_VERSION} + ${APP_BUILD} LSMinimumSystemVersion ${MIN_MACOS_VERSION} NSHighResolutionCapable @@ -268,7 +310,7 @@ cp -R "${APP_DIR}" "${DMG_STAGE_DIR}/" ln -s /Applications "${DMG_STAGE_DIR}/Applications" UNSIGNED_DMG="${WORK_DIR}/${APP_NAME}.dmg" -FINAL_DMG="${DIST_DIR}/${APP_NAME}.dmg" +FINAL_DMG="${DIST_DIR}/${APP_NAME}-${TARGET_ARCH}.dmg" log "Building DMG" hdiutil create \ From 9492400566de08a4f25d15b69051371e216d08ff Mon Sep 17 00:00:00 2001 From: Roberto Nibali Date: Sun, 15 Feb 2026 14:04:51 +0100 Subject: [PATCH 06/11] Update `.gitignore` to exclude `dist` directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1c2162e..c81a492 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ Build/ DerivedData/ *.app +dist # Swift Package Manager .swiftpm/ From 67630705f8c2af259af2f923d744d3301b3af314 Mon Sep 17 00:00:00 2001 From: Roberto Nibali Date: Sun, 15 Feb 2026 14:05:16 +0100 Subject: [PATCH 07/11] Expand `build_dmg.sh` with `--app-only`, `--open-app`, enhanced metadata, and build manifests --- scripts/build_dmg.sh | 145 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 135 insertions(+), 10 deletions(-) diff --git a/scripts/build_dmg.sh b/scripts/build_dmg.sh index 9302b16..8f5b52e 100755 --- a/scripts/build_dmg.sh +++ b/scripts/build_dmg.sh @@ -16,23 +16,30 @@ 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. +Builds Aether.app and by default also signs/notarizes/outputs dist/Aether-.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: +Required env vars (unless --app-only is used): APP_SIGN_IDENTITY Developer ID Application identity for codesign Required unless --skip-notarization is used: @@ -41,6 +48,9 @@ Required unless --skip-notarization is used: 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 @@ -62,6 +72,14 @@ while [[ $# -gt 0 ]]; do BUNDLE_ID="$2" shift 2 ;; + --app-only) + APP_ONLY=1 + shift + ;; + --open-app) + OPEN_APP=1 + shift + ;; --skip-notarization) SKIP_NOTARIZATION=1 shift @@ -139,9 +157,17 @@ resolve_release_dir() { printf '%s\n' "${path}" } +if [[ "${APP_ONLY}" == "1" ]]; then + SKIP_NOTARIZATION=1 +fi + if [[ -z "${APP_SIGN_IDENTITY}" ]]; then - echo "APP_SIGN_IDENTITY is required." >&2 - exit 1 + 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 @@ -171,20 +197,39 @@ 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 hdiutil -require_cmd ditto -require_cmd xcrun 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}, arch=${TARGET_ARCH}" +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" @@ -247,6 +292,10 @@ if [[ -d "${PROJECT_RESOURCES_DIR}" ]]; then 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" \ @@ -276,6 +325,14 @@ cat > "${APP_DIR}/Contents/Info.plist" <${APP_VERSION} CFBundleVersion ${APP_BUILD} + AetherBuildTimestamp + ${BUILD_TIMESTAMP} + AetherBuildTargetArch + ${TARGET_ARCH} + AetherBuildCommit + ${BUILD_COMMIT} + AetherLicense + ${LICENSE_NAME} LSMinimumSystemVersion ${MIN_MACOS_VERSION} NSHighResolutionCapable @@ -287,7 +344,12 @@ cat > "${APP_DIR}/Contents/Info.plist" < "${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}" < Date: Sun, 15 Feb 2026 14:05:26 +0100 Subject: [PATCH 08/11] Add `run_app_bundle.sh` for local `.app` launching and improve build metadata/docs Introduce a helper script (`run_app_bundle.sh`) to build and run `.app` bundles locally, supporting architecture-specific builds and customizable signing options. Update `BUILD.md` to document the script, expanded metadata fields, integrity artifacts, and recommended publishing steps. --- BUILD.md | 72 +++++++++++++++++++++++++++++++++++++ scripts/run_app_bundle.sh | 74 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100755 scripts/run_app_bundle.sh diff --git a/BUILD.md b/BUILD.md index 6eff234..81c7505 100644 --- a/BUILD.md +++ b/BUILD.md @@ -6,6 +6,7 @@ This project includes `scripts/build_dmg.sh` to produce a distributable macOS DM - 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 @@ -109,6 +110,10 @@ 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. @@ -121,12 +126,32 @@ 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 @@ -153,12 +178,59 @@ 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 ``` diff --git a/scripts/run_app_bundle.sh b/scripts/run_app_bundle.sh new file mode 100755 index 0000000..b0d1be0 --- /dev/null +++ b/scripts/run_app_bundle.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +TARGET_ARCH="" +SIGN_IDENTITY="${RUN_APP_SIGN_IDENTITY:--}" +EXTRA_ARGS=() + +usage() { + cat <] + +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[@]}" From 0fcff499ba7c0f4767646fd3da106d5667a2a5ff Mon Sep 17 00:00:00 2001 From: Roberto Nibali Date: Sun, 15 Feb 2026 14:10:38 +0100 Subject: [PATCH 09/11] Add custom About window with build and integrity details --- Sources/Aether/App/AetherApp.swift | 33 +++ Sources/Aether/UI/About/AboutView.swift | 333 ++++++++++++++++++++++++ 2 files changed, 366 insertions(+) create mode 100644 Sources/Aether/UI/About/AboutView.swift diff --git a/Sources/Aether/App/AetherApp.swift b/Sources/Aether/App/AetherApp.swift index 078f4ae..3ac1a80 100644 --- a/Sources/Aether/App/AetherApp.swift +++ b/Sources/Aether/App/AetherApp.swift @@ -16,6 +16,12 @@ struct AetherApp: App { } .windowStyle(.hiddenTitleBar) .commands { + CommandGroup(replacing: .appInfo) { + Button("About Aether") { + AboutWindowManager.shared.open() + } + } + CommandGroup(replacing: .newItem) { Button(translate("menu.file.openBinary")) { appState.openFile() @@ -422,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 = "About Aether" + 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/UI/About/AboutView.swift b/Sources/Aether/UI/About/AboutView.swift new file mode 100644 index 0000000..09ee133 --- /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 = "Calculating..." + @State private var isHashing = false + @State private var showLicenseSheet = false + @State private var licenseText = "License text is unavailable in this build." + @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("Beyond the binary, into the essence.") + .font(.subheadline) + .foregroundStyle(secondaryText) + Text("Version \(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("Build Details", systemImage: "wrench.and.screwdriver") + .font(.headline) + + AboutRow(label: "Built", value: buildInfo.timestampDisplay) + AboutRow(label: "Source", value: buildInfo.commit) + AboutRow(label: "Target Architecture", value: buildInfo.targetArchitecture) + AboutRow(label: "Running Architecture", value: buildInfo.runningArchitecture) + } + .padding(16) + .background(cardBackground) + } + + private var integrityCard: some View { + VStack(alignment: .leading, spacing: 10) { + Label("Integrity Fingerprint", 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("Copy SHA-256") { + copyToClipboard(executableSHA256) + } + .disabled(isHashing) + .buttonStyle(.bordered) + + Button("Copy Build Report") { + 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("License", systemImage: "doc.text") + .font(.headline) + Spacer(minLength: 0) + Button("View License Text") { + showLicenseSheet = true + } + .buttonStyle(.bordered) + } + + Text(buildInfo.license) + .font(.callout) + .foregroundStyle(secondaryText) + } + .padding(16) + .background(cardBackground) + } + + private var verificationNote: some View { + Text("Compare the SHA-256 value above with checksums published by the authoritative release source to verify this executable.") + .font(.footnote) + .foregroundStyle(secondaryText) + .padding(.horizontal, 4) + } + + private var buildReport: String { + """ + Aether Build Report + Version: \(buildInfo.version) + Build: \(buildInfo.build) + Built: \(buildInfo.timestampRaw) + Source: \(buildInfo.commit) + Target Architecture: \(buildInfo.targetArchitecture) + Running Architecture: \(buildInfo.runningArchitecture) + License: \(buildInfo.license) + Executable SHA-256: \(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() ?? "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 + } +} From e5da367b4da247c1ae89d828fb37ceffe29c8319 Mon Sep 17 00:00:00 2001 From: Roberto Nibali Date: Sun, 15 Feb 2026 14:13:16 +0100 Subject: [PATCH 10/11] Adapt About UI/menu to localization keys on de-hardcoding base --- Sources/Aether/App/AetherApp.swift | 4 +- .../Resources/en.lproj/Localizable.strings | 28 +++++++++++ Sources/Aether/UI/About/AboutView.swift | 50 +++++++++---------- 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/Sources/Aether/App/AetherApp.swift b/Sources/Aether/App/AetherApp.swift index 3ac1a80..ae7d09a 100644 --- a/Sources/Aether/App/AetherApp.swift +++ b/Sources/Aether/App/AetherApp.swift @@ -17,7 +17,7 @@ struct AetherApp: App { .windowStyle(.hiddenTitleBar) .commands { CommandGroup(replacing: .appInfo) { - Button("About Aether") { + Button(translate("about.menu.title")) { AboutWindowManager.shared.open() } } @@ -443,7 +443,7 @@ final class AboutWindowManager { let controller = NSHostingController(rootView: AboutView()) let aboutWindow = NSWindow(contentViewController: controller) - aboutWindow.title = "About Aether" + 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) diff --git a/Sources/Aether/Resources/en.lproj/Localizable.strings b/Sources/Aether/Resources/en.lproj/Localizable.strings index 2c2eb46..673389f 100644 --- a/Sources/Aether/Resources/en.lproj/Localizable.strings +++ b/Sources/Aether/Resources/en.lproj/Localizable.strings @@ -4,6 +4,34 @@ "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..."; diff --git a/Sources/Aether/UI/About/AboutView.swift b/Sources/Aether/UI/About/AboutView.swift index 09ee133..428fb03 100644 --- a/Sources/Aether/UI/About/AboutView.swift +++ b/Sources/Aether/UI/About/AboutView.swift @@ -3,10 +3,10 @@ import CryptoKit import AppKit struct AboutView: View { - @State private var executableSHA256 = "Calculating..." + @State private var executableSHA256 = translate("about.hash.calculating") @State private var isHashing = false @State private var showLicenseSheet = false - @State private var licenseText = "License text is unavailable in this build." + @State private var licenseText = translate("about.license.unavailable") @State private var didLoad = false private let buildInfo = BuildInfo.current @@ -78,10 +78,10 @@ struct AboutView: View { VStack(alignment: .leading, spacing: 5) { Text("Aether") .font(.system(size: 34, weight: .bold, design: .rounded)) - Text("Beyond the binary, into the essence.") + Text(translate("about.tagline")) .font(.subheadline) .foregroundStyle(secondaryText) - Text("Version \(buildInfo.version) (\(buildInfo.build))") + Text(String(format: translate("about.version.format"), buildInfo.version, buildInfo.build)) .font(.system(.headline, design: .monospaced)) .foregroundStyle(accentText) } @@ -94,13 +94,13 @@ struct AboutView: View { private var buildDetailsCard: some View { VStack(alignment: .leading, spacing: 10) { - Label("Build Details", systemImage: "wrench.and.screwdriver") + Label(translate("about.section.buildDetails"), systemImage: "wrench.and.screwdriver") .font(.headline) - AboutRow(label: "Built", value: buildInfo.timestampDisplay) - AboutRow(label: "Source", value: buildInfo.commit) - AboutRow(label: "Target Architecture", value: buildInfo.targetArchitecture) - AboutRow(label: "Running Architecture", value: buildInfo.runningArchitecture) + 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) @@ -108,7 +108,7 @@ struct AboutView: View { private var integrityCard: some View { VStack(alignment: .leading, spacing: 10) { - Label("Integrity Fingerprint", systemImage: "checkmark.shield") + Label(translate("about.section.integrity"), systemImage: "checkmark.shield") .font(.headline) Text(executableSHA256) @@ -122,13 +122,13 @@ struct AboutView: View { ) HStack(spacing: 10) { - Button("Copy SHA-256") { + Button(translate("about.action.copySha")) { copyToClipboard(executableSHA256) } .disabled(isHashing) .buttonStyle(.bordered) - Button("Copy Build Report") { + Button(translate("about.action.copyReport")) { copyToClipboard(buildReport) } .disabled(isHashing) @@ -147,10 +147,10 @@ struct AboutView: View { private var licenseCard: some View { VStack(alignment: .leading, spacing: 10) { HStack { - Label("License", systemImage: "doc.text") + Label(translate("about.section.license"), systemImage: "doc.text") .font(.headline) Spacer(minLength: 0) - Button("View License Text") { + Button(translate("about.action.viewLicense")) { showLicenseSheet = true } .buttonStyle(.bordered) @@ -165,7 +165,7 @@ struct AboutView: View { } private var verificationNote: some View { - Text("Compare the SHA-256 value above with checksums published by the authoritative release source to verify this executable.") + Text(translate("about.verification.note")) .font(.footnote) .foregroundStyle(secondaryText) .padding(.horizontal, 4) @@ -173,15 +173,15 @@ struct AboutView: View { private var buildReport: String { """ - Aether Build Report - Version: \(buildInfo.version) - Build: \(buildInfo.build) - Built: \(buildInfo.timestampRaw) - Source: \(buildInfo.commit) - Target Architecture: \(buildInfo.targetArchitecture) - Running Architecture: \(buildInfo.runningArchitecture) - License: \(buildInfo.license) - Executable SHA-256: \(executableSHA256) + \(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) """ } @@ -199,7 +199,7 @@ struct AboutView: View { isHashing = true DispatchQueue.global(qos: .utility).async { - let hash = Self.computeExecutableSHA256() ?? "Unavailable" + let hash = Self.computeExecutableSHA256() ?? translate("about.hash.unavailable") DispatchQueue.main.async { executableSHA256 = hash isHashing = false From 091621bfce0be033039dc104255c5a38fc7a5cce Mon Sep 17 00:00:00 2001 From: Roberto Nibali Date: Sun, 15 Feb 2026 14:17:01 +0100 Subject: [PATCH 11/11] Add About localization keys for de/fr/es/it --- .../Resources/de.lproj/Localizable.strings | 28 +++++++++++++++++++ .../Resources/es.lproj/Localizable.strings | 28 +++++++++++++++++++ .../Resources/fr.lproj/Localizable.strings | 28 +++++++++++++++++++ .../Resources/it.lproj/Localizable.strings | 28 +++++++++++++++++++ 4 files changed, 112 insertions(+) diff --git a/Sources/Aether/Resources/de.lproj/Localizable.strings b/Sources/Aether/Resources/de.lproj/Localizable.strings index c66c63c..9c2f9e4 100644 --- a/Sources/Aether/Resources/de.lproj/Localizable.strings +++ b/Sources/Aether/Resources/de.lproj/Localizable.strings @@ -4,6 +4,34 @@ "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..."; diff --git a/Sources/Aether/Resources/es.lproj/Localizable.strings b/Sources/Aether/Resources/es.lproj/Localizable.strings index 419fa98..9d4de33 100644 --- a/Sources/Aether/Resources/es.lproj/Localizable.strings +++ b/Sources/Aether/Resources/es.lproj/Localizable.strings @@ -4,6 +4,34 @@ "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..."; diff --git a/Sources/Aether/Resources/fr.lproj/Localizable.strings b/Sources/Aether/Resources/fr.lproj/Localizable.strings index cad5d0c..169c138 100644 --- a/Sources/Aether/Resources/fr.lproj/Localizable.strings +++ b/Sources/Aether/Resources/fr.lproj/Localizable.strings @@ -4,6 +4,34 @@ "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..."; diff --git a/Sources/Aether/Resources/it.lproj/Localizable.strings b/Sources/Aether/Resources/it.lproj/Localizable.strings index da8a82c..997242e 100644 --- a/Sources/Aether/Resources/it.lproj/Localizable.strings +++ b/Sources/Aether/Resources/it.lproj/Localizable.strings @@ -4,6 +4,34 @@ "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...";