Add strings catalog and update strings to be keyed (#500)

* Set up and start main content view

* Continue

* Setup flow

* No secure storage view

* Delete

* Detail

* Rename

* More create

* Empty.

* List

* Main app

* Agent and bump

* .
This commit is contained in:
Max Goedjen 2024-01-04 18:45:55 -08:00 committed by GitHub
parent 8c67ea7c73
commit c80a6f1b0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 875 additions and 134 deletions

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "SecretivePackages", name: "SecretivePackages",
platforms: [ platforms: [
.macOS(.v11) .macOS(.v12)
], ],
products: [ products: [
.library( .library(

View File

@ -139,20 +139,10 @@ extension SmartCard.Store {
guard let tokenID = tokenID else { return } guard let tokenID = tokenID else { return }
let fallbackName = NSLocalizedString("Smart Card", comment: "Smart Card") let fallbackName = NSLocalizedString("Smart Card", comment: "Smart Card")
if #available(macOS 12.0, *) { if let driverName = watcher.tokenInfo(forTokenID: tokenID)?.driverName {
if let driverName = watcher.tokenInfo(forTokenID: tokenID)?.driverName { name = driverName
name = driverName
} else {
name = fallbackName
}
} else { } else {
// Hack to read name if there's only one smart card name = fallbackName
let slotNames = TKSmartCardSlotManager().slotNames
if watcher.nonSecureEnclaveTokens.count == 1 && slotNames.count == 1 {
name = slotNames.first!
} else {
name = fallbackName
}
} }
let attributes = KeychainDictionary([ let attributes = KeychainDictionary([

View File

@ -0,0 +1,105 @@
{
"sourceLanguage" : "en",
"strings" : {
"persist_authentication_accept_button" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Leave Unlocked"
}
}
}
},
"persist_authentication_decline_button" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Do Not Unlock"
}
}
}
},
"signed_notification_description_%@" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Using secret %1$@"
}
}
}
},
"signed_notification_title_%@" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Signed Request from %1$@"
}
}
}
},
"update_notification_ignore_button" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ignore"
}
}
}
},
"update_notification_update_button" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Update"
}
}
}
},
"update_notification_update_critical_title_%@" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Critical Security Update - %1$@"
}
}
}
},
"update_notification_update_description" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Click to Update"
}
}
}
},
"update_notification_update_normal_title_%@" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Update Available - %1$@"
}
}
}
}
},
"version" : "1.0"
}

View File

@ -10,8 +10,8 @@ class Notifier {
private let notificationDelegate = NotificationDelegate() private let notificationDelegate = NotificationDelegate()
init() { init() {
let updateAction = UNNotificationAction(identifier: Constants.updateActionIdentitifier, title: "Update", options: []) let updateAction = UNNotificationAction(identifier: Constants.updateActionIdentitifier, title: String(localized: "update_notification_update_button"), options: [])
let ignoreAction = UNNotificationAction(identifier: Constants.ignoreActionIdentitifier, title: "Ignore", options: []) let ignoreAction = UNNotificationAction(identifier: Constants.ignoreActionIdentitifier, title: String(localized: "update_notification_ignore_button"), 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 @@ class Notifier {
Measurement(value: 24, unit: UnitDuration.hours) Measurement(value: 24, unit: UnitDuration.hours)
] ]
let doNotPersistAction = UNNotificationAction(identifier: Constants.doNotPersistActionIdentitifier, title: "Do Not Unlock", options: []) let doNotPersistAction = UNNotificationAction(identifier: Constants.doNotPersistActionIdentitifier, title: String(localized: "persist_authentication_decline_button"), options: [])
var allPersistenceActions = [doNotPersistAction] var allPersistenceActions = [doNotPersistAction]
let formatter = DateComponentsFormatter() let formatter = DateComponentsFormatter()
@ -40,7 +40,7 @@ class Notifier {
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("Leave Unlocked", forKey: "_actionsMenuTitle") persistAuthenticationCategory.setValue(String(localized: "persist_authentication_accept_button"), forKey: "_actionsMenuTitle")
} }
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory]) UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory, persistAuthenticationCategory])
UNUserNotificationCenter.current().delegate = notificationDelegate UNUserNotificationCenter.current().delegate = notificationDelegate
@ -62,13 +62,11 @@ class Notifier {
notificationDelegate.pendingPersistableStores[store.id.description] = store notificationDelegate.pendingPersistableStores[store.id.description] = store
let notificationCenter = UNUserNotificationCenter.current() let notificationCenter = UNUserNotificationCenter.current()
let notificationContent = UNMutableNotificationContent() let notificationContent = UNMutableNotificationContent()
notificationContent.title = "Signed Request from \(provenance.origin.displayName)" notificationContent.title = String(localized: "signed_notification_title_\(provenance.origin.displayName)")
notificationContent.subtitle = "Using secret \"\(secret.name)\"" notificationContent.subtitle = String(localized: "signed_notification_description_\(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
if #available(macOS 12.0, *) { notificationContent.interruptionLevel = .timeSensitive
notificationContent.interruptionLevel = .timeSensitive
}
if secret.requiresAuthentication && store.existingPersistedAuthenticationContext(secret: secret) == nil { if secret.requiresAuthentication && store.existingPersistedAuthenticationContext(secret: secret) == nil {
notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier notificationContent.categoryIdentifier = Constants.persistAuthenticationCategoryIdentitifier
} }
@ -85,14 +83,12 @@ class Notifier {
let notificationCenter = UNUserNotificationCenter.current() let notificationCenter = UNUserNotificationCenter.current()
let notificationContent = UNMutableNotificationContent() let notificationContent = UNMutableNotificationContent()
if update.critical { if update.critical {
if #available(macOS 12.0, *) { notificationContent.interruptionLevel = .critical
notificationContent.interruptionLevel = .critical notificationContent.title = String(localized: "update_notification_update_critical_title_\(update.name)")
}
notificationContent.title = "Critical Security Update - \(update.name)"
} else { } else {
notificationContent.title = "Update Available - \(update.name)" notificationContent.title = String(localized: "update_notification_update_normal_title_\(update.name)")
} }
notificationContent.subtitle = "Click to Update" notificationContent.subtitle = String(localized: "update_notification_update_description")
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,6 +18,8 @@
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 */; };
500B93C32B478D8400E157DE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 500B93C22B478D8400E157DE /* Localizable.xcstrings */; };
500B93C72B479E2E00E157DE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 500B93C62B479E2E00E157DE /* 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 */; };
@ -108,6 +110,8 @@
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>"; };
500B93C62B479E2E00E157DE /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
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>"; };
@ -228,6 +232,7 @@
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */, 508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */,
50617D8F23FCE48E0099B055 /* Secretive.entitlements */, 50617D8F23FCE48E0099B055 /* Secretive.entitlements */,
506772C62424784600034DED /* Credits.rtf */, 506772C62424784600034DED /* Credits.rtf */,
500B93C22B478D8400E157DE /* Localizable.xcstrings */,
50617D8823FCE48E0099B055 /* Preview Content */, 50617D8823FCE48E0099B055 /* Preview Content */,
); );
path = Secretive; path = Secretive;
@ -311,6 +316,7 @@
50A3B79824026B7600D209EA /* Info.plist */, 50A3B79824026B7600D209EA /* Info.plist */,
508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */, 508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */,
50A3B79924026B7600D209EA /* SecretAgent.entitlements */, 50A3B79924026B7600D209EA /* SecretAgent.entitlements */,
500B93C62B479E2E00E157DE /* Localizable.xcstrings */,
50A3B79224026B7600D209EA /* Preview Content */, 50A3B79224026B7600D209EA /* Preview Content */,
); );
path = SecretAgent; path = SecretAgent;
@ -444,6 +450,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */, 50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */,
500B93C32B478D8400E157DE /* 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 */,
@ -462,6 +469,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
50A3B79724026B7600D209EA /* Main.storyboard in Resources */, 50A3B79724026B7600D209EA /* Main.storyboard in Resources */,
500B93C72B479E2E00E157DE /* Localizable.xcstrings in Resources */,
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */, 50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */,
50A3B79124026B7600D209EA /* Assets.xcassets in Resources */, 50A3B79124026B7600D209EA /* Assets.xcassets in Resources */,
508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */, 508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */,
@ -606,6 +614,7 @@
STRIP_INSTALLED_PRODUCT = NO; STRIP_INSTALLED_PRODUCT = NO;
STRIP_SWIFT_SYMBOLS = NO; STRIP_SWIFT_SYMBOLS = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
}; };
name = Debug; name = Debug;
@ -664,6 +673,7 @@
STRIP_INSTALLED_PRODUCT = NO; STRIP_INSTALLED_PRODUCT = NO;
STRIP_SWIFT_SYMBOLS = NO; STRIP_SWIFT_SYMBOLS = NO;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_OPTIMIZATION_LEVEL = "-O";
}; };
name = Release; name = Release;
@ -687,6 +697,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -715,6 +726,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -827,6 +839,7 @@
STRIP_INSTALLED_PRODUCT = NO; STRIP_INSTALLED_PRODUCT = NO;
STRIP_SWIFT_SYMBOLS = NO; STRIP_SWIFT_SYMBOLS = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
}; };
name = Test; name = Test;
@ -847,6 +860,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -891,6 +905,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -915,6 +930,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -940,6 +956,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";

View File

@ -45,18 +45,18 @@ struct Secretive: App {
} }
.commands { .commands {
CommandGroup(after: CommandGroupPlacement.newItem) { CommandGroup(after: CommandGroupPlacement.newItem) {
Button("New Secret") { Button("app_menu_new_secret_button") {
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("Help") { Button("app_menu_help_button") {
NSWorkspace.shared.open(Constants.helpURL) NSWorkspace.shared.open(Constants.helpURL)
} }
} }
CommandGroup(after: .help) { CommandGroup(after: .help) {
Button("Setup Secretive") { Button("app_menu_setup_button") {
showingSetup = true showingSetup = true
} }
} }

View File

@ -0,0 +1,652 @@
{
"sourceLanguage" : "en",
"strings" : {
"\n" : {
},
"\n\n" : {
},
"agent_not_running_notice_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Secret Agent Is Not Running"
}
}
}
},
"agent_running_notice_detail_description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "SecretAgent is a process that runs in the background to sign requests, so you don't need to keep Secretive open all the time.\\n\\n**You can close Secretive, and everything will still keep working.**"
}
}
}
},
"agent_running_notice_detail_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "SecretAgent is Running"
}
}
}
},
"agent_running_notice_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Agent is Running"
}
}
}
},
"agent_setup_notice_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Setup Secretive"
}
}
}
},
"app_menu_help_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Help"
}
}
}
},
"app_menu_new_secret_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "New Secret"
}
}
}
},
"app_menu_setup_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Setup Secretive"
}
}
}
},
"app_not_in_applications_notice_detail_description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Secretive needs to be in your Applications folder to work properly. Please move it and relaunch."
}
}
}
},
"app_not_in_applications_notice_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Secretive Is Not in Applications Folder"
}
}
}
},
"create_secret_cancel_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cancel"
}
}
}
},
"create_secret_create_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Create"
}
}
}
},
"create_secret_name_label" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Name:"
}
}
}
},
"create_secret_name_placeholder" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Shhhhh"
}
}
}
},
"create_secret_notify_description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "No authentication is required while your Mac is unlocked, but you will be notified when a secret is used."
}
}
}
},
"create_secret_notify_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notify"
}
}
}
},
"create_secret_require_authentication_description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "You will be required to authenticate using Touch ID, Apple Watch, or password before each use."
}
}
}
},
"create_secret_require_authentication_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Require Authentication"
}
}
}
},
"create_secret_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Create a New Secret"
}
}
}
},
"delete_confirmation_cancel_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Don't Delete"
}
}
}
},
"delete_confirmation_confirm_name_label" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Confirm Name:"
}
}
}
},
"delete_confirmation_delete_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Delete"
}
}
}
},
"delete_confirmation_description_%@_%@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "If you delete %1$@, you will not be able to recover it. Type \"%2$@\" to confirm."
}
}
}
},
"delete_confirmation_title_%@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Delete %1$@?"
}
}
}
},
"empty_store_modifiable_click_here_description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Create a new one by clicking here."
}
}
}
},
"empty_store_modifiable_click_here_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "No Secrets"
}
}
}
},
"empty_store_modifiable_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "No Secrets"
}
}
}
},
"empty_store_nonmodifiable_description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Use your Smart Card's management tool to create a secret."
}
}
}
},
"empty_store_nonmodifiable_supported_key_types" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Secretive supports EC256, EC384, RSA1024, and RSA2048 keys."
}
}
}
},
"empty_store_nonmodifiable_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "No Secrets"
}
}
}
},
"no_secure_storage_description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Your Mac doesn't have a Secure Enclave, and there's not a compatible Smart Card inserted."
}
}
}
},
"no_secure_storage_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "No Secure Storage Available"
}
}
}
},
"no_secure_storage_yubico_link" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "If you're looking to add one to your Mac, the YubiKey 5 Series are great."
}
}
}
},
"rename_cancel_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cancel"
}
}
}
},
"rename_rename_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Rename"
}
}
}
},
"rename_title_%@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Type your new name for %1$@ below."
}
}
}
},
"secret_detail_md5_fingerprint_label" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "MD5 Fingerprint"
}
}
}
},
"secret_detail_public_key_label" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Public Key"
}
}
}
},
"secret_detail_public_key_path_label" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Public Key Path"
}
}
}
},
"secret_detail_sha256_fingerprint_label" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "SHA256 Fingerprint"
}
}
}
},
"secret_list_delete_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Delete"
}
}
}
},
"secret_list_rename_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Rename"
}
}
}
},
"setup_agent_activity_monitor_description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "This helper app is called **Secret Agent** and you may see it in Activity Manager from time to time."
}
}
}
},
"setup_agent_description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Secretive needs to set up a helper app to work properly. It will sign requests from SSH clients in the background, so you don't need to keep the main Secretive app open."
}
}
}
},
"setup_agent_install_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Install"
}
}
}
},
"setup_agent_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Setup Secret Agent"
}
}
}
},
"setup_ssh_add_for_me_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Add it For Me"
}
}
}
},
"setup_ssh_add_to_config_button_%@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Add to %1$@"
}
}
}
},
"setup_ssh_added_manually_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "I Added it Manually"
}
}
}
},
"setup_ssh_description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Add this line to your shell config telling SSH to talk to Secret Agent when it wants to authenticate. Secretive can either do this for you automatically, or you can copy and paste this into your config file."
}
}
}
},
"setup_ssh_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Configure your SSH Agent"
}
}
}
},
"setup_step_complete_symbol" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "✓"
}
}
}
},
"setup_third_party_faq_link" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "If you're trying to set up a third party app, check out the FAQ."
}
}
}
},
"setup_updates_description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Secretive will periodically check with GitHub to see if there's a new release. If you see any network requests to GitHub, that's why."
}
}
}
},
"setup_updates_ok" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "OK"
}
}
}
},
"setup_updates_readmore" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Read more about this here."
}
}
}
},
"setup_updates_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Updates"
}
}
}
},
"update_critical_notice_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Critical Security Update Required"
}
}
}
},
"update_ignore_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ignore"
}
}
}
},
"update_normal_notice_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Update Available"
}
}
}
},
"update_release_notes_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Release Notes"
}
}
}
},
"update_test_notice_title" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Test Build"
}
}
}
},
"update_update_button" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Update"
}
}
}
},
"update_version_name_%@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Secretive %1$@"
}
}
}
}
},
"version" : "1.0"
}

View File

@ -64,15 +64,15 @@ extension ContentView {
} }
} }
var updateNoticeContent: (String, Color)? { var updateNoticeContent: (LocalizedStringKey, Color)? {
guard let update = updater.update else { return nil } guard let update = updater.update else { return nil }
if update.critical { if update.critical {
return ("Critical Security Update Required", .red) return ("update_critical_notice_title", .red)
} else { } else {
if updater.testBuild { if updater.testBuild {
return ("Test Build", .blue) return ("update_test_notice_title", .blue)
} else { } else {
return ("Update Available", .orange) return ("update_normal_notice_title", .orange)
} }
} }
} }
@ -121,9 +121,9 @@ extension ContentView {
}, label: { }, label: {
Group { Group {
if hasRunSetup && !agentStatusChecker.running { if hasRunSetup && !agentStatusChecker.running {
Text("Secret Agent Is Not Running") Text("agent_not_running_notice_title")
} else { } else {
Text("Setup Secretive") Text("agent_setup_notice_title")
} }
} }
.font(.headline) .font(.headline)
@ -138,7 +138,7 @@ extension ContentView {
showingAgentInfo = true showingAgentInfo = true
}, label: { }, label: {
HStack { HStack {
Text("Agent is Running") Text("agent_running_notice_title")
.font(.headline) .font(.headline)
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white) .foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
Circle() Circle()
@ -149,10 +149,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("SecretAgent is Running") Text("agent_running_notice_detail_title")
.font(.title) .font(.title)
.padding(5) .padding(5)
Text("SecretAgent is a process that runs in the background to sign requests, so you don't need to keep Secretive open all the time.\n\n**You can close Secretive, and everything will still keep working.**") Text("agent_running_notice_detail_description")
.frame(width: 300) .frame(width: 300)
} }
.padding() .padding()
@ -166,7 +166,7 @@ extension ContentView {
showingAppPathNotice = true showingAppPathNotice = true
}, label: { }, label: {
Group { Group {
Text("Secretive Is Not in Applications Folder") Text("app_not_in_applications_notice_title")
} }
.font(.headline) .font(.headline)
.foregroundColor(.white) .foregroundColor(.white)
@ -178,7 +178,7 @@ extension ContentView {
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 64) .frame(width: 64)
Text("Secretive needs to be in your Applications folder to work properly. Please move it and relaunch.") Text("app_not_in_applications_notice_detail_description")
.frame(maxWidth: 300) .frame(maxWidth: 300)
} }
.padding() .padding()

View File

@ -3,7 +3,7 @@ import UniformTypeIdentifiers
struct CopyableView: View { struct CopyableView: View {
var title: String var title: LocalizedStringKey
var image: Image var image: Image
var text: String var text: String
@ -122,9 +122,9 @@ struct CopyableView: View {
struct CopyableView_Previews: PreviewProvider { struct CopyableView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Group { Group {
CopyableView(title: "Title", image: Image(systemName: "figure.wave"), text: "Hello world.") CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Hello world.")
.padding() .padding()
CopyableView(title: "Title", image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ") CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
.padding() .padding()
} }
} }

View File

@ -14,43 +14,30 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
HStack { HStack {
VStack { VStack {
HStack { HStack {
Text("Create a New Secret") Text("create_secret_title")
.font(.largeTitle) .font(.largeTitle)
Spacer() Spacer()
} }
HStack { HStack {
Text("Name:") Text("create_secret_name_label")
TextField("Shhhhh", text: $name) TextField("create_secret_name_placeholder", text: $name)
.focusable() .focusable()
} }
if #available(macOS 12.0, *) { 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: "Require Authentication", description: "You will be required to authenticate using Touch ID, Apple Watch, or password before each use.", thumbnail: AuthenticationView()), ThumbnailPickerView.Item(value: false, name: "create_secret_notify_title",
ThumbnailPickerView.Item(value: false, name: "Notify", description: "create_secret_notify_description",
description: "No authentication is required while your Mac is unlocked, but you will be notified when a secret is used.", thumbnail: NotificationView())
thumbnail: NotificationView()) ], selection: $requiresAuthentication)
], selection: $requiresAuthentication)
} else {
HStack {
VStack(spacing: 20) {
Picker("", selection: $requiresAuthentication) {
Text("Requires Authentication (Biometrics or Password) before each use").tag(true)
Text("Authentication not required when Mac is unlocked").tag(false)
}
.pickerStyle(RadioGroupPickerStyle())
Spacer(minLength: 10)
}
}
}
} }
} }
HStack { HStack {
Spacer() Spacer()
Button("Cancel") { Button("create_secret_cancel_button") {
showing = false showing = false
} }
.keyboardShortcut(.cancelAction) .keyboardShortcut(.cancelAction)
Button("Create", action: save) Button("create_secret_create_button", action: save)
.disabled(name.isEmpty) .disabled(name.isEmpty)
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
} }
@ -109,11 +96,11 @@ extension ThumbnailPickerView {
struct Item<ValueType: Hashable>: Identifiable { struct Item<ValueType: Hashable>: Identifiable {
let id = UUID() let id = UUID()
let value: ValueType let value: ValueType
let name: String let name: LocalizedStringKey
let description: String let description: LocalizedStringKey
let thumbnail: AnyView let thumbnail: AnyView
init<ViewType: View>(value: ValueType, name: String, description: String, thumbnail: ViewType) { init<ViewType: View>(value: ValueType, name: LocalizedStringKey, description: LocalizedStringKey, thumbnail: ViewType) {
self.value = value self.value = value
self.name = name self.name = name
self.description = description self.description = description
@ -138,7 +125,6 @@ extension ThumbnailPickerView {
} }
@available(macOS 12.0, *)
struct SystemBackgroundView: View { struct SystemBackgroundView: View {
let anchor: UnitPoint let anchor: UnitPoint
@ -157,7 +143,6 @@ struct SystemBackgroundView: View {
} }
} }
@available(macOS 12.0, *)
struct AuthenticationView: View { struct AuthenticationView: View {
var body: some View { var body: some View {
@ -169,15 +154,15 @@ struct AuthenticationView: View {
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.foregroundColor(Color(.systemRed)) .foregroundColor(Color(.systemRed))
Text("Touch ID Prompt") Text(verbatim: "Touch ID Prompt")
.font(.headline) .font(.headline)
.foregroundColor(.primary) .foregroundColor(.primary)
.redacted(reason: .placeholder) .redacted(reason: .placeholder)
VStack { VStack {
Text("Touch ID Detail prompt.Detail two.") Text(verbatim: "Touch ID Detail prompt.Detail two.")
.font(.caption2) .font(.caption2)
.foregroundColor(.primary) .foregroundColor(.primary)
Text("Touch ID Detail prompt.Detail two.") Text(verbatim: "Touch ID Detail prompt.Detail two.")
.font(.caption2) .font(.caption2)
.foregroundColor(.primary) .foregroundColor(.primary)
} }
@ -203,7 +188,6 @@ struct AuthenticationView: View {
} }
@available(macOS 12.0, *)
struct NotificationView: View { struct NotificationView: View {
var body: some View { var body: some View {
@ -223,10 +207,10 @@ struct NotificationView: View {
.frame(width: 64, height: 64) .frame(width: 64, height: 64)
.foregroundColor(.primary) .foregroundColor(.primary)
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Secretive") Text(verbatim: "Secretive")
.font(.title) .font(.title)
.foregroundColor(.primary) .foregroundColor(.primary)
Text("Secretive wants to sign") Text(verbatim: "Secretive wants to sign")
.font(.body) .font(.body)
.foregroundColor(.primary) .foregroundColor(.primary)
} }
@ -253,14 +237,10 @@ struct CreateSecretView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Group { Group {
CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true)) CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true))
if #available(macOS 12.0, *) {
AuthenticationView().environment(\.colorScheme, .dark) AuthenticationView().environment(\.colorScheme, .dark)
AuthenticationView().environment(\.colorScheme, .light) AuthenticationView().environment(\.colorScheme, .light)
NotificationView().environment(\.colorScheme, .dark) NotificationView().environment(\.colorScheme, .dark)
NotificationView().environment(\.colorScheme, .light) NotificationView().environment(\.colorScheme, .light)
} else {
// Fallback on earlier versions
}
} }
} }
} }

View File

@ -18,24 +18,24 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
.padding() .padding()
VStack { VStack {
HStack { HStack {
Text("Delete \(secret.name)?").bold() Text("delete_confirmation_title_\(secret.name)").bold()
Spacer() Spacer()
} }
HStack { HStack {
Text("If you delete \(secret.name), you will not be able to recover it. Type \"\(secret.name)\" to confirm.") Text("delete_confirmation_description_\(secret.name)_\(secret.name)")
Spacer() Spacer()
} }
HStack { HStack {
Text("Confirm Name:") Text("delete_confirmation_confirm_name_label")
TextField(secret.name, text: $confirm) TextField(secret.name, text: $confirm)
} }
} }
} }
HStack { HStack {
Spacer() Spacer()
Button("Delete", action: delete) Button("delete_confirmation_delete_button", action: delete)
.disabled(confirm != secret.name) .disabled(confirm != secret.name)
Button("Don't Delete") { Button("delete_confirmation_cancel_button") {
dismissalBlock(false) dismissalBlock(false)
} }
.keyboardShortcut(.cancelAction) .keyboardShortcut(.cancelAction)

View File

@ -9,11 +9,11 @@ struct EmptyStoreView: View {
var body: some View { var body: some View {
if store is AnySecretStoreModifiable { if store is AnySecretStoreModifiable {
NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: $activeSecret) { NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: $activeSecret) {
Text("No Secrets") Text("empty_store_modifiable_title")
} }
} else { } else {
NavigationLink(destination: EmptyStoreImmutableView(), tag: Constants.emptyStoreTag, selection: $activeSecret) { NavigationLink(destination: EmptyStoreImmutableView(), tag: Constants.emptyStoreTag, selection: $activeSecret) {
Text("No Secrets") Text("empty_store_nonmodifiable_title")
} }
} }
} }
@ -23,7 +23,7 @@ extension EmptyStoreView {
enum Constants { enum Constants {
static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag" static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag"
static let emptyStoreTag: AnyHashable = "emptyStoreModifiableTag" static let emptyStoreTag: AnyHashable = "emptyStoreTag"
} }
} }
@ -32,9 +32,9 @@ struct EmptyStoreImmutableView: View {
var body: some View { var body: some View {
VStack { VStack {
Text("No Secrets").bold() Text("empty_store_nonmodifiable_title").bold()
Text("Use your Smart Card's management tool to create a secret.") Text("empty_store_nonmodifiable_description")
Text("Secretive supports EC256, EC384, RSA1024, and RSA2048 keys.") Text("empty_store_nonmodifiable_supported_key_types")
}.frame(maxWidth: .infinity, maxHeight: .infinity) }.frame(maxWidth: .infinity, maxHeight: .infinity)
} }
@ -63,8 +63,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("No Secrets").bold() Text("empty_store_modifiable_click_here_title").bold()
Text("Create a new one by clicking here.") Text("empty_store_modifiable_click_here_description")
Spacer() Spacer()
}.frame(maxWidth: .infinity, maxHeight: .infinity) }.frame(maxWidth: .infinity, maxHeight: .infinity)
} }

View File

@ -4,9 +4,10 @@ struct NoStoresView: View {
var body: some View { var body: some View {
VStack { VStack {
Text("No Secure Storage Available").bold() Text("no_secure_storage_title")
Text("Your Mac doesn't have a Secure Enclave, and there's not a compatible Smart Card inserted.") .bold()
Link("If you're looking to add one to your Mac, the YubiKey 5 Series are great.", destination: URL(string: "https://www.yubico.com/products/compare-yubikey-5-series/")!) Text("no_secure_storage_description")
Link("no_secure_storage_yubico_link", 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("Type your new name for \"\(secret.name)\" below.") Text("rename_title_\(secret.name)")
Spacer() Spacer()
} }
HStack { HStack {
@ -28,10 +28,10 @@ struct RenameSecretView<StoreType: SecretStoreModifiable>: View {
} }
HStack { HStack {
Spacer() Spacer()
Button("Rename", action: rename) Button("rename_rename_button", action: rename)
.disabled(newName.count == 0) .disabled(newName.count == 0)
.keyboardShortcut(.return) .keyboardShortcut(.return)
Button("Cancel") { Button("rename_cancel_button") {
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: "SHA256 Fingerprint", image: Image(systemName: "touchid"), text: keyWriter.openSSHSHA256Fingerprint(secret: secret)) CopyableView(title: "secret_detail_sha256_fingerprint_label", image: Image(systemName: "touchid"), text: keyWriter.openSSHSHA256Fingerprint(secret: secret))
Spacer() Spacer()
.frame(height: 20) .frame(height: 20)
CopyableView(title: "MD5 Fingerprint", image: Image(systemName: "touchid"), text: keyWriter.openSSHMD5Fingerprint(secret: secret)) CopyableView(title: "secret_detail_md5_fingerprint_label", image: Image(systemName: "touchid"), text: keyWriter.openSSHMD5Fingerprint(secret: secret))
Spacer() Spacer()
.frame(height: 20) .frame(height: 20)
CopyableView(title: "Public Key", image: Image(systemName: "key"), text: keyString) CopyableView(title: "secret_detail_public_key_label", image: Image(systemName: "key"), text: keyString)
Spacer() Spacer()
.frame(height: 20) .frame(height: 20)
CopyableView(title: "Public Key Path", image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.publicKeyPath(for: secret)) CopyableView(title: "secret_detail_public_key_path_label", image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.publicKeyPath(for: secret))
Spacer() Spacer()
} }
} }

View File

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

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("") Text("setup_step_complete_symbol")
.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: String let title: LocalizedStringKey
let image: Image let image: Image
let bodyText: String let bodyText: LocalizedStringKey
let buttonTitle: String let buttonTitle: LocalizedStringKey
let buttonAction: () -> Void let buttonAction: () -> Void
let content: Content let content: Content
init(title: String, image: Image, bodyText: String, buttonTitle: String, buttonAction: @escaping () -> Void = {}, @ViewBuilder content: () -> Content) { init(title: LocalizedStringKey, image: Image, bodyText: LocalizedStringKey, buttonTitle: LocalizedStringKey, 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 Secret Agent", SetupStepView(title: "setup_agent_title",
image: Image(nsImage: NSApplication.shared.applicationIconImage), image: Image(nsImage: NSApplication.shared.applicationIconImage),
bodyText: "Secretive needs to set up a helper app to work properly. It will sign requests from SSH clients in the background, so you don't need to keep the main Secretive app open.", bodyText: "setup_agent_description",
buttonTitle: "Install", buttonTitle: "setup_agent_install_button",
buttonAction: install) { buttonAction: install) {
(Text("This helper app is called ") + Text("Secret Agent").bold().underline() + Text(" and you may see it in Activity Manager from time to time.")) Text("setup_agent_activity_monitor_description")
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
} }
@ -170,12 +170,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: "Configure your SSH Agent", SetupStepView(title: "setup_ssh_title",
image: Image(systemName: "terminal"), image: Image(systemName: "terminal"),
bodyText: "Add this line to your shell config telling SSH to talk to Secret Agent when it wants to authenticate. Secretive can either do this for you automatically, or you can copy and paste this into your config file.", bodyText: "setup_ssh_description",
buttonTitle: "I Added it Manually", buttonTitle: "setup_ssh_added_manually_button",
buttonAction: buttonAction) { buttonAction: buttonAction) {
Link("If you're trying to set up a third party app, check out the FAQ.", destination: URL(string: "https://github.com/maxgoedjen/secretive/blob/main/APP_CONFIG.md")!) Link("setup_third_party_faq_link", 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)
@ -183,8 +183,8 @@ struct SSHAgentSetupView: View {
.padding() .padding()
} }
}.pickerStyle(SegmentedPickerStyle()) }.pickerStyle(SegmentedPickerStyle())
CopyableView(title: "Add to \(selectedShellInstruction.shellConfigPath)", image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text) CopyableView(title: "setup_ssh_add_to_config_button_\(selectedShellInstruction.shellConfigPath)", image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text)
Button("Add it For Me") { Button("setup_ssh_add_for_me_button") {
let controller = ShellConfigurationController() let controller = ShellConfigurationController()
if controller.addToShell(shellInstructions: selectedShellInstruction) { if controller.addToShell(shellInstructions: selectedShellInstruction) {
buttonAction() buttonAction()
@ -214,12 +214,12 @@ struct UpdaterExplainerView: View {
let buttonAction: () -> Void let buttonAction: () -> Void
var body: some View { var body: some View {
SetupStepView(title: "Updates", SetupStepView(title: "setup_updates_title",
image: Image(systemName: "dot.radiowaves.left.and.right"), image: Image(systemName: "dot.radiowaves.left.and.right"),
bodyText: "Secretive will periodically check with GitHub to see if there's a new release. If you see any network requests to GitHub, that's why.", bodyText: "setup_updates_description",
buttonTitle: "Okay", buttonTitle: "setup_updates_ok",
buttonAction: buttonAction) { buttonAction: buttonAction) {
Link("Read more about this here.", destination: SetupView.Constants.updaterFAQURL) Link("setup_updates_readmore", destination: SetupView.Constants.updaterFAQURL)
} }
} }

View File

@ -9,20 +9,20 @@ struct UpdateDetailView<UpdaterType: Updater>: View {
var body: some View { var body: some View {
VStack { VStack {
Text("Secretive \(update.name)").font(.title) Text("update_version_name_\(update.name)").font(.title)
GroupBox(label: Text("Release Notes")) { GroupBox(label: Text("update_release_notes_title")) {
ScrollView { ScrollView {
attributedBody attributedBody
} }
} }
HStack { HStack {
if !update.critical { if !update.critical {
Button("Ignore") { Button("update_ignore_button") {
updater.ignore(release: update) updater.ignore(release: update)
} }
Spacer() Spacer()
} }
Button("Update") { Button("update_update_button") {
NSWorkspace.shared.open(update.html_url) NSWorkspace.shared.open(update.html_url)
} }
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
@ -34,7 +34,7 @@ struct UpdateDetailView<UpdaterType: Updater>: View {
} }
var attributedBody: Text { var attributedBody: Text {
var text = Text("") var text = Text(verbatim: "")
for line in update.body.split(whereSeparator: \.isNewline) { for line in update.body.split(whereSeparator: \.isNewline) {
let attributed: Text let attributed: Text
let split = line.split(separator: " ") let split = line.split(separator: " ")