Switch to generated localized string symbols (#607)

* Switch to string symbols

* Names

* Cleanup packages

* Cleanup packages

* Remove namespace

* More cleanup

* Fix extra param.

* Use swiftbuild
This commit is contained in:
Max Goedjen 2025-08-17 22:26:13 -05:00 committed by GitHub
parent 83ecc15332
commit 9749cd6f3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 528 additions and 535 deletions

View File

@ -23,10 +23,7 @@ jobs:
- name: Set Environment - name: Set Environment
run: sudo xcrun xcode-select -s /Applications/Xcode_26_beta_5.app run: sudo xcrun xcode-select -s /Applications/Xcode_26_beta_5.app
- name: Test - name: Test
run: | run: swift build --build-system swiftbuild --package-path Sources/Packages
pushd Sources/Packages
swift test
popd
build: build:
# runs-on: macOS-latest # runs-on: macOS-latest
runs-on: macos-15 runs-on: macos-15

View File

@ -11,7 +11,4 @@ jobs:
- name: Set Environment - name: Set Environment
run: sudo xcrun xcode-select -s /Applications/Xcode_26_beta_5.app run: sudo xcrun xcode-select -s /Applications/Xcode_26_beta_5.app
- name: Test - name: Test
run: | run: swift build --build-system swiftbuild --package-path Sources/Packages
pushd Sources/Packages
swift test
popd

View File

@ -5,6 +5,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "SecretivePackages", name: "SecretivePackages",
defaultLocalization: "en",
platforms: [ platforms: [
.macOS(.v14) .macOS(.v14)
], ],
@ -34,6 +35,7 @@ let package = Package(
.target( .target(
name: "SecretKit", name: "SecretKit",
dependencies: [], dependencies: [],
resources: [localization],
swiftSettings: swiftSettings swiftSettings: swiftSettings
), ),
.testTarget( .testTarget(
@ -44,16 +46,19 @@ let package = Package(
.target( .target(
name: "SecureEnclaveSecretKit", name: "SecureEnclaveSecretKit",
dependencies: ["SecretKit"], dependencies: ["SecretKit"],
resources: [localization],
swiftSettings: swiftSettings swiftSettings: swiftSettings
), ),
.target( .target(
name: "SmartCardSecretKit", name: "SmartCardSecretKit",
dependencies: ["SecretKit"], dependencies: ["SecretKit"],
resources: [localization],
swiftSettings: swiftSettings swiftSettings: swiftSettings
), ),
.target( .target(
name: "SecretAgentKit", name: "SecretAgentKit",
dependencies: ["SecretKit", "SecretAgentKitHeaders"], dependencies: ["SecretKit", "SecretAgentKitHeaders"],
resources: [localization],
swiftSettings: swiftSettings swiftSettings: swiftSettings
), ),
.systemLibrary( .systemLibrary(
@ -66,6 +71,7 @@ let package = Package(
.target( .target(
name: "Brief", name: "Brief",
dependencies: [], dependencies: [],
resources: [localization],
swiftSettings: swiftSettings swiftSettings: swiftSettings
), ),
.testTarget( .testTarget(
@ -75,6 +81,10 @@ let package = Package(
] ]
) )
var localization: Resource {
.process("../../Localizable.xcstrings")
}
var swiftSettings: [PackageDescription.SwiftSetting] { var swiftSettings: [PackageDescription.SwiftSetting] {
[ [
.swiftLanguageMode(.v6), .swiftLanguageMode(.v6),

View File

@ -0,0 +1 @@

View File

@ -50,16 +50,16 @@ extension SecureEnclave {
func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws { func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) async throws {
let newContext = LAContext() let newContext = LAContext()
newContext.touchIDAuthenticationAllowableReuseDuration = duration newContext.touchIDAuthenticationAllowableReuseDuration = duration
newContext.localizedCancelTitle = String(localized: "auth_context_request_deny_button") newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
let formatter = DateComponentsFormatter() let formatter = DateComponentsFormatter()
formatter.unitsStyle = .spellOut formatter.unitsStyle = .spellOut
formatter.allowedUnits = [.hour, .minute, .day] formatter.allowedUnits = [.hour, .minute, .day]
if let durationString = formatter.string(from: duration) { if let durationString = formatter.string(from: duration) {
newContext.localizedReason = String(localized: "auth_context_persist_for_duration_\(secret.name)_\(durationString)") newContext.localizedReason = String(localized: .authContextPersistForDuration(secretName: secret.name, duration: durationString))
} else { } else {
newContext.localizedReason = String(localized: "auth_context_persist_for_duration_unknown_\(secret.name)") newContext.localizedReason = String(localized: .authContextPersistForDurationUnknown(secretName: secret.name))
} }
let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason) let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason)
guard success else { return } guard success else { return }

View File

@ -15,7 +15,7 @@ extension SecureEnclave {
CryptoKit.SecureEnclave.isAvailable CryptoKit.SecureEnclave.isAvailable
} }
public let id = UUID() public let id = UUID()
public let name = String(localized: "secure_enclave") public let name = String(localized: .secureEnclave)
private let persistentAuthenticationHandler = PersistentAuthenticationHandler() private let persistentAuthenticationHandler = PersistentAuthenticationHandler()
/// Initializes a Store. /// Initializes a Store.
@ -105,10 +105,10 @@ extension SecureEnclave {
context = existing.context context = existing.context
} else { } else {
let newContext = LAContext() let newContext = LAContext()
newContext.localizedCancelTitle = String(localized: "auth_context_request_deny_button") newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
context = newContext context = newContext
} }
context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)") context.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: provenance.origin.displayName, secretName: secret.name))
let attributes = KeychainDictionary([ let attributes = KeychainDictionary([
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecAttrKeyClass: kSecAttrKeyClassPrivate,
@ -138,8 +138,8 @@ extension SecureEnclave {
public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool { public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
let context = LAContext() let context = LAContext()
context.localizedReason = String(localized: "auth_context_request_verify_description_\(secret.name)") context.localizedReason = String(localized: .authContextRequestVerifyDescription(secretName: secret.name))
context.localizedCancelTitle = String(localized: "auth_context_request_deny_button") context.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
let attributes = KeychainDictionary([ let attributes = KeychainDictionary([
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecAttrKeyClass: kSecAttrKeyClassPrivate,
@ -240,7 +240,7 @@ extension SecureEnclave.Store {
nil)! nil)!
let wrapped: [SecureEnclave.Secret] = publicTyped.map { let wrapped: [SecureEnclave.Secret] = publicTyped.map {
let name = $0[kSecAttrLabel] as? String ?? String(localized: "unnamed_secret") let name = $0[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret)
let id = $0[kSecAttrApplicationLabel] as! Data let id = $0[kSecAttrApplicationLabel] as! Data
let publicKeyRef = $0[kSecValueRef] as! SecKey let publicKeyRef = $0[kSecValueRef] as! SecKey
let publicKeyAttributes = SecKeyCopyAttributes(publicKeyRef) as! [CFString: Any] let publicKeyAttributes = SecKeyCopyAttributes(publicKeyRef) as! [CFString: Any]

View File

@ -9,7 +9,7 @@ extension SmartCard {
@MainActor @Observable fileprivate final class State { @MainActor @Observable fileprivate final class State {
var isAvailable = false var isAvailable = false
var name = String(localized: "smart_card") var name = String(localized: .smartCard)
var secrets: [Secret] = [] var secrets: [Secret] = []
let watcher = TKTokenWatcher() let watcher = TKTokenWatcher()
var tokenID: String? = nil var tokenID: String? = nil
@ -63,8 +63,8 @@ extension SmartCard {
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data { public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) async throws -> Data {
guard let tokenID = await state.tokenID else { fatalError() } guard let tokenID = await state.tokenID else { fatalError() }
let context = LAContext() let context = LAContext()
context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)") context.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: provenance.origin.displayName, secretName: secret.name))
context.localizedCancelTitle = String(localized: "auth_context_request_deny_button") context.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
let attributes = KeychainDictionary([ let attributes = KeychainDictionary([
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecAttrKeyClass: kSecAttrKeyClassPrivate,
@ -162,7 +162,7 @@ extension SmartCard.Store {
@MainActor private func loadSecrets() { @MainActor private func loadSecrets() {
guard let tokenID = state.tokenID else { return } guard let tokenID = state.tokenID else { return }
let fallbackName = String(localized: "smart_card") let fallbackName = String(localized: .smartCard)
if let driverName = state.watcher.tokenInfo(forTokenID: tokenID)?.driverName { if let driverName = state.watcher.tokenInfo(forTokenID: tokenID)?.driverName {
state.name = driverName state.name = driverName
} else { } else {
@ -180,7 +180,7 @@ extension SmartCard.Store {
SecItemCopyMatching(attributes, &untyped) SecItemCopyMatching(attributes, &untyped)
guard let typed = untyped as? [[CFString: Any]] else { return } guard let typed = untyped as? [[CFString: Any]] else { return }
let wrapped = typed.map { let wrapped = typed.map {
let name = $0[kSecAttrLabel] as? String ?? String(localized: "unnamed_secret") let name = $0[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret)
let tokenID = $0[kSecAttrApplicationLabel] as! Data let tokenID = $0[kSecAttrApplicationLabel] as! Data
let algorithm = Algorithm(secAttr: $0[kSecAttrKeyType] as! NSNumber) let algorithm = Algorithm(secAttr: $0[kSecAttrKeyType] as! NSNumber)
let keySize = $0[kSecAttrKeySizeInBits] as! Int let keySize = $0[kSecAttrKeySizeInBits] as! Int
@ -207,8 +207,8 @@ extension SmartCard.Store {
/// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged. /// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged.
public func encrypt(data: Data, with secret: SecretType) throws -> Data { public func encrypt(data: Data, with secret: SecretType) throws -> Data {
let context = LAContext() let context = LAContext()
context.localizedReason = String(localized: "auth_context_request_encrypt_description_\(secret.name)") context.localizedReason = String(localized: .authContextRequestEncryptDescription(secretName: secret.name))
context.localizedCancelTitle = String(localized: "auth_context_request_deny_button") context.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
let attributes = KeychainDictionary([ let attributes = KeychainDictionary([
kSecAttrKeyType: secret.algorithm.secAttrKeyType, kSecAttrKeyType: secret.algorithm.secAttrKeyType,
kSecAttrKeySizeInBits: secret.keySize, kSecAttrKeySizeInBits: secret.keySize,
@ -236,8 +236,8 @@ extension SmartCard.Store {
public func decrypt(data: Data, with secret: SecretType) async throws -> Data { public func decrypt(data: Data, with secret: SecretType) async throws -> Data {
guard let tokenID = await state.tokenID else { fatalError() } guard let tokenID = await state.tokenID else { fatalError() }
let context = LAContext() let context = LAContext()
context.localizedReason = String(localized: "auth_context_request_decrypt_description_\(secret.name)") context.localizedReason = String(localized: .authContextRequestDecryptDescription(secretName: secret.name))
context.localizedCancelTitle = String(localized: "auth_context_request_deny_button") context.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
let attributes = KeychainDictionary([ let attributes = KeychainDictionary([
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecAttrKeyClass: kSecAttrKeyClassPrivate,

View File

@ -10,8 +10,8 @@ final class Notifier: Sendable {
private let notificationDelegate = NotificationDelegate() private let notificationDelegate = NotificationDelegate()
init() { init() {
let updateAction = UNNotificationAction(identifier: Constants.updateActionIdentitifier, title: String(localized: "update_notification_update_button"), options: []) let updateAction = UNNotificationAction(identifier: Constants.updateActionIdentitifier, title: String(localized: .updateNotificationUpdateButton), options: [])
let ignoreAction = UNNotificationAction(identifier: Constants.ignoreActionIdentitifier, title: String(localized: "update_notification_ignore_button"), options: []) let ignoreAction = UNNotificationAction(identifier: Constants.ignoreActionIdentitifier, title: String(localized: .updateNotificationIgnoreButton), options: [])
let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: []) let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: [])
let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: []) let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: [])
@ -22,7 +22,7 @@ final class Notifier: Sendable {
Measurement(value: 24, unit: UnitDuration.hours) Measurement(value: 24, unit: UnitDuration.hours)
] ]
let doNotPersistAction = UNNotificationAction(identifier: Constants.doNotPersistActionIdentitifier, title: String(localized: "persist_authentication_decline_button"), options: []) let doNotPersistAction = UNNotificationAction(identifier: Constants.doNotPersistActionIdentitifier, title: String(localized: .persistAuthenticationDeclineButton), options: [])
var allPersistenceActions = [doNotPersistAction] var allPersistenceActions = [doNotPersistAction]
let formatter = DateComponentsFormatter() let formatter = DateComponentsFormatter()
@ -41,7 +41,7 @@ final class Notifier: Sendable {
let persistAuthenticationCategory = UNNotificationCategory(identifier: Constants.persistAuthenticationCategoryIdentitifier, actions: allPersistenceActions, intentIdentifiers: [], options: []) let persistAuthenticationCategory = UNNotificationCategory(identifier: Constants.persistAuthenticationCategoryIdentitifier, actions: allPersistenceActions, intentIdentifiers: [], options: [])
if persistAuthenticationCategory.responds(to: Selector(("actionsMenuTitle"))) { if persistAuthenticationCategory.responds(to: Selector(("actionsMenuTitle"))) {
persistAuthenticationCategory.setValue(String(localized: "persist_authentication_accept_button"), forKey: "_actionsMenuTitle") persistAuthenticationCategory.setValue(String(localized: .persistAuthenticationAcceptButton), forKey: "_actionsMenuTitle")
} }
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory]) UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory])
UNUserNotificationCenter.current().delegate = notificationDelegate UNUserNotificationCenter.current().delegate = notificationDelegate
@ -64,8 +64,8 @@ final class Notifier: Sendable {
await notificationDelegate.state.setPending(secret: secret, store: store) await notificationDelegate.state.setPending(secret: secret, store: store)
let notificationCenter = UNUserNotificationCenter.current() let notificationCenter = UNUserNotificationCenter.current()
let notificationContent = UNMutableNotificationContent() let notificationContent = UNMutableNotificationContent()
notificationContent.title = String(localized: "signed_notification_title_\(provenance.origin.displayName)") notificationContent.title = String(localized: .signedNotificationTitle(appName: provenance.origin.displayName))
notificationContent.subtitle = String(localized: "signed_notification_description_\(secret.name)") notificationContent.subtitle = String(localized: .signedNotificationDescription(secretName: secret.name))
notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description notificationContent.userInfo[Constants.persistSecretIDKey] = secret.id.description
notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description notificationContent.userInfo[Constants.persistStoreIDKey] = store.id.description
notificationContent.interruptionLevel = .timeSensitive notificationContent.interruptionLevel = .timeSensitive
@ -85,11 +85,11 @@ final class Notifier: Sendable {
let notificationContent = UNMutableNotificationContent() let notificationContent = UNMutableNotificationContent()
if update.critical { if update.critical {
notificationContent.interruptionLevel = .critical notificationContent.interruptionLevel = .critical
notificationContent.title = String(localized: "update_notification_update_critical_title_\(update.name)") notificationContent.title = String(localized: .updateNotificationUpdateCriticalTitle(updateName: update.name))
} else { } else {
notificationContent.title = String(localized: "update_notification_update_normal_title_\(update.name)") notificationContent.title = String(localized: .updateNotificationUpdateNormalTitle(updateName: update.name))
} }
notificationContent.subtitle = String(localized: "update_notification_update_description") notificationContent.subtitle = String(localized: .updateNotificationUpdateDescription)
notificationContent.body = update.body notificationContent.body = update.body
notificationContent.categoryIdentifier = update.critical ? Constants.criticalUpdateCategoryIdentitifier : Constants.updateCategoryIdentitifier notificationContent.categoryIdentifier = update.critical ? Constants.criticalUpdateCategoryIdentitifier : Constants.updateCategoryIdentitifier
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil) let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)

View File

@ -18,8 +18,9 @@
5003EF612780081600DF2006 /* SmartCardSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF602780081600DF2006 /* SmartCardSecretKit */; }; 5003EF612780081600DF2006 /* SmartCardSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF602780081600DF2006 /* SmartCardSecretKit */; };
5003EF632780081B00DF2006 /* SecureEnclaveSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */; }; 5003EF632780081B00DF2006 /* SecureEnclaveSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF622780081B00DF2006 /* SecureEnclaveSecretKit */; };
5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF642780081B00DF2006 /* SmartCardSecretKit */; }; 5003EF652780081B00DF2006 /* SmartCardSecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF642780081B00DF2006 /* SmartCardSecretKit */; };
5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
5008C2402E52792400507AC2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; }; 5008C2402E52792400507AC2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; };
500B93C32B478D8400E157DE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 500B93C22B478D8400E157DE /* Localizable.xcstrings */; }; 5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */; };
501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; }; 501421622781262300BBAA70 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 501421612781262300BBAA70 /* Brief */; };
501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 501421652781268000BBAA70 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; }; 50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
@ -51,7 +52,6 @@
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; }; 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; };
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; }; 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; };
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; }; 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
50E9CF422B51D596004AB36D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 500B93C22B478D8400E157DE /* Localizable.xcstrings */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -102,7 +102,7 @@
50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; }; 50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; };
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; }; 5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
500B93C22B478D8400E157DE /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; }; 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Localizable.xcstrings; sourceTree = SOURCE_ROOT; };
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; }; 50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; }; 50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; }; 5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
@ -210,7 +210,7 @@
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */, 508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */,
50617D8F23FCE48E0099B055 /* Secretive.entitlements */, 50617D8F23FCE48E0099B055 /* Secretive.entitlements */,
506772C62424784600034DED /* Credits.rtf */, 506772C62424784600034DED /* Credits.rtf */,
500B93C22B478D8400E157DE /* Localizable.xcstrings */, 5008C23D2E525D8200507AC2 /* Localizable.xcstrings */,
50617D8823FCE48E0099B055 /* Preview Content */, 50617D8823FCE48E0099B055 /* Preview Content */,
); );
path = Secretive; path = Secretive;
@ -404,7 +404,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */, 50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */,
500B93C32B478D8400E157DE /* Localizable.xcstrings in Resources */, 5008C23E2E525D8900507AC2 /* Localizable.xcstrings in Resources */,
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */, 50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */,
506772C72424784600034DED /* Credits.rtf in Resources */, 506772C72424784600034DED /* Credits.rtf in Resources */,
508BF28E25B4F005009EFB7E /* InternetAccessPolicy.plist in Resources */, 508BF28E25B4F005009EFB7E /* InternetAccessPolicy.plist in Resources */,
@ -416,7 +416,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
50A3B79724026B7600D209EA /* Main.storyboard in Resources */, 50A3B79724026B7600D209EA /* Main.storyboard in Resources */,
50E9CF422B51D596004AB36D /* Localizable.xcstrings in Resources */, 5008C2412E52D18700507AC2 /* Localizable.xcstrings in Resources */,
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */, 50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */,
508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */, 508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */,
5008C2402E52792400507AC2 /* Assets.xcassets in Resources */, 5008C2402E52792400507AC2 /* Assets.xcassets in Resources */,

View File

@ -59,18 +59,18 @@ struct Secretive: App {
} }
.commands { .commands {
CommandGroup(after: CommandGroupPlacement.newItem) { CommandGroup(after: CommandGroupPlacement.newItem) {
Button("app_menu_new_secret_button") { Button(.appMenuNewSecretButton) {
showingCreation = true showingCreation = true
} }
.keyboardShortcut(KeyboardShortcut(KeyEquivalent("N"), modifiers: [.command, .shift])) .keyboardShortcut(KeyboardShortcut(KeyEquivalent("N"), modifiers: [.command, .shift]))
} }
CommandGroup(replacing: .help) { CommandGroup(replacing: .help) {
Button("app_menu_help_button") { Button(.appMenuHelpButton) {
NSWorkspace.shared.open(Constants.helpURL) NSWorkspace.shared.open(Constants.helpURL)
} }
} }
CommandGroup(after: .help) { CommandGroup(after: .help) {
Button("app_menu_setup_button") { Button(.appMenuSetupButton) {
showingSetup = true showingSetup = true
} }
} }

View File

@ -70,15 +70,15 @@ extension ContentView {
} }
} }
var updateNoticeContent: (LocalizedStringKey, Color)? { var updateNoticeContent: (LocalizedStringResource, Color)? {
guard let update = updater.update else { return nil } guard let update = updater.update else { return nil }
if update.critical { if update.critical {
return ("update_critical_notice_title", .red) return (.updateCriticalNoticeTitle, .red)
} else { } else {
if updater.testBuild { if updater.testBuild {
return ("update_test_notice_title", .blue) return (.updateTestNoticeTitle, .blue)
} else { } else {
return ("update_normal_notice_title", .orange) return (.updateNormalNoticeTitle, .orange)
} }
} }
} }
@ -127,13 +127,13 @@ extension ContentView {
}, label: { }, label: {
Group { Group {
if hasRunSetup && !agentStatusChecker.running { if hasRunSetup && !agentStatusChecker.running {
Text("agent_not_running_notice_title") Text(.agentNotRunningNoticeTitle)
} else { } else {
Text("agent_setup_notice_title") Text(.agentSetupNoticeTitle)
} }
} }
.font(.headline) .font(.headline)
.foregroundColor(.white)
}) })
.buttonStyle(ToolbarButtonStyle(color: .orange)) .buttonStyle(ToolbarButtonStyle(color: .orange))
} }
@ -144,7 +144,7 @@ extension ContentView {
showingAgentInfo = true showingAgentInfo = true
}, label: { }, label: {
HStack { HStack {
Text("agent_running_notice_title") Text(.agentRunningNoticeTitle)
.font(.headline) .font(.headline)
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white) .foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
Circle() Circle()
@ -155,10 +155,10 @@ extension ContentView {
.buttonStyle(ToolbarButtonStyle(lightColor: .black.opacity(0.05), darkColor: .white.opacity(0.05))) .buttonStyle(ToolbarButtonStyle(lightColor: .black.opacity(0.05), darkColor: .white.opacity(0.05)))
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { .popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
VStack { VStack {
Text("agent_running_notice_detail_title") Text(.agentRunningNoticeDetailTitle)
.font(.title) .font(.title)
.padding(5) .padding(5)
Text("agent_running_notice_detail_description") Text(.agentRunningNoticeDetailDescription)
.frame(width: 300) .frame(width: 300)
} }
.padding() .padding()
@ -172,7 +172,7 @@ extension ContentView {
showingAppPathNotice = true showingAppPathNotice = true
}, label: { }, label: {
Group { Group {
Text("app_not_in_applications_notice_title") Text(.appNotInApplicationsNoticeTitle)
} }
.font(.headline) .font(.headline)
.foregroundColor(.white) .foregroundColor(.white)
@ -184,7 +184,7 @@ extension ContentView {
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 64) .frame(width: 64)
Text("app_not_in_applications_notice_detail_description") Text(.appNotInApplicationsNoticeDetailDescription)
.frame(maxWidth: 300) .frame(maxWidth: 300)
} }
.padding() .padding()

View File

@ -3,7 +3,7 @@ import UniformTypeIdentifiers
struct CopyableView: View { struct CopyableView: View {
var title: LocalizedStringKey var title: LocalizedStringResource
var image: Image var image: Image
var text: String var text: String

View File

@ -14,30 +14,30 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
HStack { HStack {
VStack { VStack {
HStack { HStack {
Text("create_secret_title") Text(.createSecretTitle)
.font(.largeTitle) .font(.largeTitle)
Spacer() Spacer()
} }
HStack { HStack {
Text("create_secret_name_label") Text(.createSecretNameLabel)
TextField("create_secret_name_placeholder", text: $name) TextField(String(localized: .createSecretNamePlaceholder), text: $name)
.focusable() .focusable()
} }
ThumbnailPickerView(items: [ ThumbnailPickerView(items: [
ThumbnailPickerView.Item(value: true, name: "create_secret_require_authentication_title", description: "create_secret_require_authentication_description", thumbnail: AuthenticationView()), ThumbnailPickerView.Item(value: true, name: .createSecretRequireAuthenticationTitle, description: .createSecretRequireAuthenticationDescription, thumbnail: AuthenticationView()),
ThumbnailPickerView.Item(value: false, name: "create_secret_notify_title", ThumbnailPickerView.Item(value: false, name: .createSecretNotifyTitle,
description: "create_secret_notify_description", description: .createSecretNotifyDescription,
thumbnail: NotificationView()) thumbnail: NotificationView())
], selection: $requiresAuthentication) ], selection: $requiresAuthentication)
} }
} }
HStack { HStack {
Spacer() Spacer()
Button("create_secret_cancel_button") { Button(.createSecretCancelButton) {
showing = false showing = false
} }
.keyboardShortcut(.cancelAction) .keyboardShortcut(.cancelAction)
Button("create_secret_create_button", action: save) Button(.createSecretCreateButton, action: save)
.disabled(name.isEmpty) .disabled(name.isEmpty)
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
} }
@ -98,11 +98,11 @@ extension ThumbnailPickerView {
struct Item<InnerValueType: Hashable>: Identifiable { struct Item<InnerValueType: Hashable>: Identifiable {
let id = UUID() let id = UUID()
let value: InnerValueType let value: InnerValueType
let name: LocalizedStringKey let name: LocalizedStringResource
let description: LocalizedStringKey let description: LocalizedStringResource
let thumbnail: AnyView let thumbnail: AnyView
init<ViewType: View>(value: InnerValueType, name: LocalizedStringKey, description: LocalizedStringKey, thumbnail: ViewType) { init<ViewType: View>(value: InnerValueType, name: LocalizedStringResource, description: LocalizedStringResource, thumbnail: ViewType) {
self.value = value self.value = value
self.name = name self.name = name
self.description = description self.description = description

View File

@ -18,24 +18,24 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
.padding() .padding()
VStack { VStack {
HStack { HStack {
Text("delete_confirmation_title_\(secret.name)").bold() Text(.deleteConfirmationTitle(secretName: secret.name)).bold()
Spacer() Spacer()
} }
HStack { HStack {
Text("delete_confirmation_description_\(secret.name)_\(secret.name)") Text(.deleteConfirmationDescription(secretName: secret.name, confirmSecretName: secret.name))
Spacer() Spacer()
} }
HStack { HStack {
Text("delete_confirmation_confirm_name_label") Text(.deleteConfirmationConfirmNameLabel)
TextField(secret.name, text: $confirm) TextField(secret.name, text: $confirm)
} }
} }
} }
HStack { HStack {
Spacer() Spacer()
Button("delete_confirmation_delete_button", action: delete) Button(.deleteConfirmationDeleteButton, action: delete)
.disabled(confirm != secret.name) .disabled(confirm != secret.name)
Button("delete_confirmation_cancel_button") { Button(.deleteConfirmationCancelButton) {
dismissalBlock(false) dismissalBlock(false)
} }
.keyboardShortcut(.cancelAction) .keyboardShortcut(.cancelAction)

View File

@ -18,9 +18,9 @@ struct EmptyStoreImmutableView: View {
var body: some View { var body: some View {
VStack { VStack {
Text("empty_store_nonmodifiable_title").bold() Text(.emptyStoreNonmodifiableTitle).bold()
Text("empty_store_nonmodifiable_description") Text(.emptyStoreNonmodifiableDescription)
Text("empty_store_nonmodifiable_supported_key_types") Text(.emptyStoreNonmodifiableSupportedKeyTypes)
}.frame(maxWidth: .infinity, maxHeight: .infinity) }.frame(maxWidth: .infinity, maxHeight: .infinity)
} }
@ -49,8 +49,8 @@ struct EmptyStoreModifiableView: View {
path.addLine(to: CGPoint(x: g.size.width - 3, y: 0)) path.addLine(to: CGPoint(x: g.size.width - 3, y: 0))
}.fill() }.fill()
}.frame(height: (windowGeometry.size.height/2) - 20).padding() }.frame(height: (windowGeometry.size.height/2) - 20).padding()
Text("empty_store_modifiable_click_here_title").bold() Text(.emptyStoreModifiableClickHereTitle).bold()
Text("empty_store_modifiable_click_here_description") Text(.emptyStoreModifiableClickHereDescription)
Spacer() Spacer()
}.frame(maxWidth: .infinity, maxHeight: .infinity) }.frame(maxWidth: .infinity, maxHeight: .infinity)
} }

View File

@ -4,10 +4,10 @@ struct NoStoresView: View {
var body: some View { var body: some View {
VStack { VStack {
Text("no_secure_storage_title") Text(.noSecureStorageTitle)
.bold() .bold()
Text("no_secure_storage_description") Text(.noSecureStorageDescription)
Link("no_secure_storage_yubico_link", destination: URL(string: "https://www.yubico.com/products/compare-yubikey-5-series/")!) Link(.noSecureStorageYubicoLink, destination: URL(string: "https://www.yubico.com/products/compare-yubikey-5-series/")!)
}.padding() }.padding()
} }

View File

@ -18,7 +18,7 @@ struct RenameSecretView<StoreType: SecretStoreModifiable>: View {
.padding() .padding()
VStack { VStack {
HStack { HStack {
Text("rename_title_\(secret.name)") Text(.renameTitle(secretName: secret.name))
Spacer() Spacer()
} }
HStack { HStack {
@ -28,10 +28,10 @@ struct RenameSecretView<StoreType: SecretStoreModifiable>: View {
} }
HStack { HStack {
Spacer() Spacer()
Button("rename_rename_button", action: rename) Button(.renameRenameButton, action: rename)
.disabled(newName.count == 0) .disabled(newName.count == 0)
.keyboardShortcut(.return) .keyboardShortcut(.return)
Button("rename_cancel_button") { Button(.renameCancelButton) {
dismissalBlock(false) dismissalBlock(false)
}.keyboardShortcut(.cancelAction) }.keyboardShortcut(.cancelAction)
} }

View File

@ -12,16 +12,16 @@ struct SecretDetailView<SecretType: Secret>: View {
ScrollView { ScrollView {
Form { Form {
Section { Section {
CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "touchid"), text: keyWriter.openSSHSHA256Fingerprint(secret: secret)) CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "touchid"), text: keyWriter.openSSHSHA256Fingerprint(secret: secret))
Spacer() Spacer()
.frame(height: 20) .frame(height: 20)
CopyableView(title: "secret_detail_md5_fingerprint_label", image: Image(systemName: "touchid"), text: keyWriter.openSSHMD5Fingerprint(secret: secret)) CopyableView(title: .secretDetailMd5FingerprintLabel, image: Image(systemName: "touchid"), text: keyWriter.openSSHMD5Fingerprint(secret: secret))
Spacer() Spacer()
.frame(height: 20) .frame(height: 20)
CopyableView(title: "secret_detail_public_key_label", image: Image(systemName: "key"), text: keyString) CopyableView(title: .secretDetailPublicKeyLabel, image: Image(systemName: "key"), text: keyString)
Spacer() Spacer()
.frame(height: 20) .frame(height: 20)
CopyableView(title: "secret_detail_public_key_path_label", image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.publicKeyPath(for: secret)) CopyableView(title: .secretDetailPublicKeyPathLabel, image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.publicKeyPath(for: secret))
Spacer() Spacer()
} }
} }

View File

@ -39,10 +39,10 @@ struct SecretListItemView: View {
.contextMenu { .contextMenu {
if store is AnySecretStoreModifiable { if store is AnySecretStoreModifiable {
Button(action: { isRenaming = true }) { Button(action: { isRenaming = true }) {
Text("secret_list_rename_button") Text(.secretListRenameButton)
} }
Button(action: { isDeleting = true }) { Button(action: { isDeleting = true }) {
Text("secret_list_delete_button") Text(.secretListDeleteButton)
} }
} }
} }

View File

@ -61,7 +61,7 @@ struct StepView: View {
Circle() Circle()
.foregroundColor(.green) .foregroundColor(.green)
.frame(width: Constants.circleWidth, height: Constants.circleWidth) .frame(width: Constants.circleWidth, height: Constants.circleWidth)
Text("setup_step_complete_symbol") Text(.setupStepCompleteSymbol)
.foregroundColor(.white) .foregroundColor(.white)
.bold() .bold()
} else { } else {
@ -101,14 +101,14 @@ extension StepView {
struct SetupStepView<Content> : View where Content : View { struct SetupStepView<Content> : View where Content : View {
let title: LocalizedStringKey let title: LocalizedStringResource
let image: Image let image: Image
let bodyText: LocalizedStringKey let bodyText: LocalizedStringResource
let buttonTitle: LocalizedStringKey let buttonTitle: LocalizedStringResource
let buttonAction: () -> Void let buttonAction: () -> Void
let content: Content let content: Content
init(title: LocalizedStringKey, image: Image, bodyText: LocalizedStringKey, buttonTitle: LocalizedStringKey, buttonAction: @escaping () -> Void = {}, @ViewBuilder content: () -> Content) { init(title: LocalizedStringResource, image: Image, bodyText: LocalizedStringResource, buttonTitle: LocalizedStringResource, buttonAction: @escaping () -> Void = {}, @ViewBuilder content: () -> Content) {
self.title = title self.title = title
self.image = image self.image = image
self.bodyText = bodyText self.bodyText = bodyText
@ -145,12 +145,12 @@ struct SecretAgentSetupView: View {
let buttonAction: () -> Void let buttonAction: () -> Void
var body: some View { var body: some View {
SetupStepView(title: "setup_agent_title", SetupStepView(title: .setupAgentTitle,
image: Image(nsImage: NSApplication.shared.applicationIconImage), image: Image(nsImage: NSApplication.shared.applicationIconImage),
bodyText: "setup_agent_description", bodyText: .setupAgentDescription,
buttonTitle: "setup_agent_install_button", buttonTitle: .setupAgentInstallButton,
buttonAction: install) { buttonAction: install) {
Text("setup_agent_activity_monitor_description") Text(.setupAgentActivityMonitorDescription)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
} }
@ -172,12 +172,12 @@ struct SSHAgentSetupView: View {
@State private var selectedShellInstruction: ShellConfigInstruction = controller.shellInstructions.first! @State private var selectedShellInstruction: ShellConfigInstruction = controller.shellInstructions.first!
var body: some View { var body: some View {
SetupStepView(title: "setup_ssh_title", SetupStepView(title: .setupSshTitle,
image: Image(systemName: "terminal"), image: Image(systemName: "terminal"),
bodyText: "setup_ssh_description", bodyText: .setupSshDescription,
buttonTitle: "setup_ssh_added_manually_button", buttonTitle: .setupSshAddedManuallyButton,
buttonAction: buttonAction) { buttonAction: buttonAction) {
Link("setup_third_party_faq_link", destination: URL(string: "https://github.com/maxgoedjen/secretive/blob/main/APP_CONFIG.md")!) Link(.setupThirdPartyFaqLink, destination: URL(string: "https://github.com/maxgoedjen/secretive/blob/main/APP_CONFIG.md")!)
Picker(selection: $selectedShellInstruction, label: EmptyView()) { Picker(selection: $selectedShellInstruction, label: EmptyView()) {
ForEach(SSHAgentSetupView.controller.shellInstructions) { instruction in ForEach(SSHAgentSetupView.controller.shellInstructions) { instruction in
Text(instruction.shell) Text(instruction.shell)
@ -185,8 +185,8 @@ struct SSHAgentSetupView: View {
.padding() .padding()
} }
}.pickerStyle(SegmentedPickerStyle()) }.pickerStyle(SegmentedPickerStyle())
CopyableView(title: "setup_ssh_add_to_config_button_\(selectedShellInstruction.shellConfigPath)", image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text) CopyableView(title: .setupSshAddToConfigButton(configPath: selectedShellInstruction.shellConfigPath), image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text)
Button("setup_ssh_add_for_me_button") { Button(.setupSshAddForMeButton) {
let controller = ShellConfigurationController() let controller = ShellConfigurationController()
if controller.addToShell(shellInstructions: selectedShellInstruction) { if controller.addToShell(shellInstructions: selectedShellInstruction) {
buttonAction() buttonAction()
@ -216,12 +216,12 @@ struct UpdaterExplainerView: View {
let buttonAction: () -> Void let buttonAction: () -> Void
var body: some View { var body: some View {
SetupStepView(title: "setup_updates_title", SetupStepView(title: .setupUpdatesTitle,
image: Image(systemName: "dot.radiowaves.left.and.right"), image: Image(systemName: "dot.radiowaves.left.and.right"),
bodyText: "setup_updates_description", bodyText: .setupUpdatesDescription,
buttonTitle: "setup_updates_ok", buttonTitle: .setupUpdatesOk,
buttonAction: buttonAction) { buttonAction: buttonAction) {
Link("setup_updates_readmore", destination: SetupView.Constants.updaterFAQURL) Link(.setupUpdatesReadmore, destination: SetupView.Constants.updaterFAQURL)
} }
} }

View File

@ -9,22 +9,22 @@ struct UpdateDetailView: View {
var body: some View { var body: some View {
VStack { VStack {
Text("update_version_name_\(update.name)").font(.title) Text(.updateVersionName(updateName: update.name)).font(.title)
GroupBox(label: Text("update_release_notes_title")) { GroupBox(label: Text(.updateReleaseNotesTitle)) {
ScrollView { ScrollView {
attributedBody attributedBody
} }
} }
HStack { HStack {
if !update.critical { if !update.critical {
Button("update_ignore_button") { Button(.updateIgnoreButton) {
Task { Task {
await updater.ignore(release: update) await updater.ignore(release: update)
} }
} }
Spacer() Spacer()
} }
Button("update_update_button") { Button(.updateUpdateButton) {
NSWorkspace.shared.open(update.html_url) NSWorkspace.shared.open(update.html_url)
} }
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)