diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f4ef350498..f1b2e26a6f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2197,6 +2197,19 @@ 569277C529DEE09D00B633EF /* ContinueSetUpModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569277C329DEE09D00B633EF /* ContinueSetUpModelTests.swift */; }; 56B234BF2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B234BE2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift */; }; 56B234C02A84EFD800F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B234BE2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift */; }; + 56BA1E752BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E742BAAF70F001CF69F /* SSLErrorPageTabExtension.swift */; }; + 56BA1E762BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E742BAAF70F001CF69F /* SSLErrorPageTabExtension.swift */; }; + 56BA1E772BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E742BAAF70F001CF69F /* SSLErrorPageTabExtension.swift */; }; + 56BA1E7F2BAB2D29001CF69F /* ErrorPageTabExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */; }; + 56BA1E802BAB2E43001CF69F /* ErrorPageTabExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */; }; + 56BA1E822BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E812BAC506F001CF69F /* SSLErrorPageUserScript.swift */; }; + 56BA1E832BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E812BAC506F001CF69F /* SSLErrorPageUserScript.swift */; }; + 56BA1E842BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E812BAC506F001CF69F /* SSLErrorPageUserScript.swift */; }; + 56BA1E872BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E862BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift */; }; + 56BA1E882BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E862BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift */; }; + 56BA1E8A2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */; }; + 56BA1E8B2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */; }; + 56BA1E8C2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */; }; 56CEE90E2B7A725B00CF10AA /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */; }; 56CEE90F2B7A725C00CF10AA /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */; }; 56CEE9102B7A72FE00CF10AA /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */; }; @@ -4066,6 +4079,11 @@ 569277C029DDCBB500B633EF /* HomePageContinueSetUpModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageContinueSetUpModel.swift; sourceTree = ""; }; 569277C329DEE09D00B633EF /* ContinueSetUpModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueSetUpModelTests.swift; sourceTree = ""; }; 56B234BE2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarUrlExtensionsTests.swift; sourceTree = ""; }; + 56BA1E742BAAF70F001CF69F /* SSLErrorPageTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLErrorPageTabExtension.swift; sourceTree = ""; }; + 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPageTabExtensionTest.swift; sourceTree = ""; }; + 56BA1E812BAC506F001CF69F /* SSLErrorPageUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLErrorPageUserScript.swift; sourceTree = ""; }; + 56BA1E862BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLErrorPageUserScriptTests.swift; sourceTree = ""; }; + 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateTrustEvaluator.swift; sourceTree = ""; }; 56CEE9092B7A66C500CF10AA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 56D145E729E6BB6300E3488A /* CapturingDataImportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturingDataImportProvider.swift; sourceTree = ""; }; @@ -6355,6 +6373,7 @@ 4BB88B4F25B7BA2B006F6B06 /* TabInstrumentation.swift */, 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */, 4B9579202AC687170062CA31 /* HardwareModel.swift */, + 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */, ); path = Utilities; sourceTree = ""; @@ -6562,6 +6581,14 @@ path = Mocks; sourceTree = ""; }; + 56BA1E852BAC820D001CF69F /* UserScripts */ = { + isa = PBXGroup; + children = ( + 56BA1E862BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift */, + ); + path = UserScripts; + sourceTree = ""; + }; 7B1E819A27C8874900FF0E60 /* Autofill */ = { isa = PBXGroup; children = ( @@ -7315,6 +7342,7 @@ AA585D93248FD31400E9A3E2 /* UnitTests */ = { isa = PBXGroup; children = ( + 56BA1E852BAC820D001CF69F /* UserScripts */, C13909F22B85FD60001626ED /* Autofill */, 5629846D2AC460DF00AC20EB /* Sync */, B6A5A28C25B962CB00AA7ADA /* App */, @@ -8408,6 +8436,7 @@ B66260E529ACAE4B00E9E3EE /* NavigationHotkeyHandler.swift */, B66260DC29AC5D4300E9E3EE /* NavigationProtectionTabExtension.swift */, B626A76C29928B1600053070 /* TestsClosureNavigationResponder.swift */, + 56BA1E742BAAF70F001CF69F /* SSLErrorPageTabExtension.swift */, ); path = TabExtensions; sourceTree = ""; @@ -8485,6 +8514,7 @@ isa = PBXGroup; children = ( B684121B2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift */, + 56BA1E812BAC506F001CF69F /* SSLErrorPageUserScript.swift */, ); path = ErrorPage; sourceTree = ""; @@ -8664,6 +8694,7 @@ B626A7632992506A00053070 /* SerpHeadersNavigationResponderTests.swift */, 1D1C36E529FB019C001FA40C /* HistoryTabExtensionTests.swift */, 1D8C2FE42B70F4C4005E4BBD /* TabSnapshotExtensionTests.swift */, + 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */, ); path = TabExtensionsTests; sourceTree = ""; @@ -10374,6 +10405,7 @@ 3706FAC2293F65D500E42796 /* FaviconSelector.swift in Sources */, B696AFFC2AC5924800C93203 /* FileLineError.swift in Sources */, F1D43AEF2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */, + 56BA1E8B2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */, B6E1491129A5C30A00AAFBE8 /* FBProtectionTabExtension.swift in Sources */, 3706FAC4293F65D500E42796 /* PrintingUserScript.swift in Sources */, 1D01A3D92B88DF8B00FE8150 /* PreferencesSyncView.swift in Sources */, @@ -10526,6 +10558,7 @@ 3706FB35293F65D500E42796 /* FlatButton.swift in Sources */, 3706FB36293F65D500E42796 /* PinnedTabView.swift in Sources */, 3706FB37293F65D500E42796 /* DataEncryption.swift in Sources */, + 56BA1E762BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */, 4B9DB0362A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */, 37197EA82942443D00394917 /* BrowserTabViewController.swift in Sources */, 3706FB39293F65D500E42796 /* PrivacyDashboardPopover.swift in Sources */, @@ -10893,6 +10926,7 @@ B6685E4029A606190043D2EE /* WorkspaceProtocol.swift in Sources */, 3706FC2F293F65D500E42796 /* MouseOverButton.swift in Sources */, 3706FC30293F65D500E42796 /* FireInfoViewController.swift in Sources */, + 56BA1E832BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */, 3706FC31293F65D500E42796 /* PermissionButton.swift in Sources */, 3706FC32293F65D500E42796 /* MoreOptionsMenu.swift in Sources */, 3706FC34293F65D500E42796 /* PermissionAuthorizationViewController.swift in Sources */, @@ -11115,6 +11149,7 @@ 3706FE04293F661700E42796 /* TreeControllerTests.swift in Sources */, 3706FE05293F661700E42796 /* DownloadsWebViewMock.m in Sources */, 3706FE06293F661700E42796 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, + 56BA1E7F2BAB2D29001CF69F /* ErrorPageTabExtensionTest.swift in Sources */, 3706FE07293F661700E42796 /* PasswordManagementItemListModelTests.swift in Sources */, 3706FE08293F661700E42796 /* WKWebsiteDataStoreExtensionTests.swift in Sources */, 3706FE09293F661700E42796 /* VariantManagerTests.swift in Sources */, @@ -11292,6 +11327,7 @@ 3706FE79293F661700E42796 /* AppearancePreferencesTests.swift in Sources */, 3706FE7A293F661700E42796 /* FirePopoverViewModelTests.swift in Sources */, 7B09CBAA2BA4BE8200CF245B /* NetworkProtectionPixelEventTests.swift in Sources */, + 56BA1E882BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */, 3706FE7B293F661700E42796 /* HistoryStoringMock.swift in Sources */, 562984702AC4610100AC20EB /* SyncPreferencesTests.swift in Sources */, 3706FE7C293F661700E42796 /* LocalBookmarkStoreTests.swift in Sources */, @@ -11793,6 +11829,7 @@ 4B957A272AC7AE700062CA31 /* PrivacyDashboardPopover.swift in Sources */, 4B957A282AC7AE700062CA31 /* TestsClosureNavigationResponder.swift in Sources */, 4B957A292AC7AE700062CA31 /* RootView.swift in Sources */, + 56BA1E772BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */, 4B37EE7C2B4CFF8000A89A61 /* HomePageRemoteMessagingRequest.swift in Sources */, 4B957A2A2AC7AE700062CA31 /* AddressBarTextField.swift in Sources */, 4B957A2B2AC7AE700062CA31 /* FocusRingView.swift in Sources */, @@ -11937,9 +11974,11 @@ 4B520F652BA5573A006405C7 /* WaitlistThankYouView.swift in Sources */, 4B957AA02AC7AE700062CA31 /* BookmarkManager.swift in Sources */, 4B957AA12AC7AE700062CA31 /* AboutModel.swift in Sources */, + 56BA1E8C2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */, 4B957AA22AC7AE700062CA31 /* PasswordManagementCreditCardItemView.swift in Sources */, 3158B1552B0BF75900AF130C /* LoginItem+DataBrokerProtection.swift in Sources */, 4B957AA32AC7AE700062CA31 /* NSTextFieldExtension.swift in Sources */, + 56BA1E842BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */, 4B957AA42AC7AE700062CA31 /* BWManagement.swift in Sources */, 4B957AA52AC7AE700062CA31 /* FireproofDomainsContainer.swift in Sources */, 4B957AA62AC7AE700062CA31 /* ExternalAppSchemeHandler.swift in Sources */, @@ -12533,6 +12572,7 @@ 4BE65485271FCD7B008D1D63 /* LoginFaviconView.swift in Sources */, 4B0511CA262CAA5A00F6079C /* FireproofDomainsViewController.swift in Sources */, AA4D700725545EF800C3411E /* URLEventHandler.swift in Sources */, + 56BA1E8A2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */, 1D8057C82A83CAEE00F4FED6 /* SupportedOsChecker.swift in Sources */, AA92127725ADA07900600CD4 /* WKWebViewExtension.swift in Sources */, AAAB9114288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift in Sources */, @@ -12792,6 +12832,7 @@ 4B723E1226B0006E00E14D75 /* DataImport.swift in Sources */, 7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */, B6085D092743AAB600A9C456 /* FireproofDomains.xcdatamodeld in Sources */, + 56BA1E752BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */, 85589E8227BBB8630038AD11 /* HomePageView.swift in Sources */, B6BF5D932947199A006742B1 /* SerpHeadersNavigationResponder.swift in Sources */, 569277C129DDCBB500B633EF /* HomePageContinueSetUpModel.swift in Sources */, @@ -12866,6 +12907,7 @@ B66260E629ACAE4B00E9E3EE /* NavigationHotkeyHandler.swift in Sources */, EA0BA3A9272217E6002A0B6C /* ClickToLoadUserScript.swift in Sources */, AAA892EA250A4CEF005B37B2 /* WindowControllersManager.swift in Sources */, + 56BA1E822BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */, 85C5991B27D10CF000E605B2 /* FireAnimationView.swift in Sources */, B6B4D1CA2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */, AA6197C4276B314D008396F0 /* FaviconUrlReference.swift in Sources */, @@ -13150,6 +13192,7 @@ B67C6C472654C643006C872E /* FileManagerExtensionTests.swift in Sources */, 4BD57C042AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */, B69B50482726C5C200758A2B /* StatisticsLoaderTests.swift in Sources */, + 56BA1E872BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */, 142879DA24CE1179005419BB /* SuggestionViewModelTests.swift in Sources */, 4B9292BC2667103100AD2C21 /* BookmarkSidebarTreeControllerTests.swift in Sources */, 4B9DB05A2A983B55000927DB /* MockWaitlistRequest.swift in Sources */, @@ -13296,6 +13339,7 @@ B6AA64732994B43300D99CD6 /* FutureExtensionTests.swift in Sources */, B603975329C1FFAE00902A34 /* ExpectedNavigationExtension.swift in Sources */, EA1E52B52798CF98002EC53C /* ClickToLoadModelTests.swift in Sources */, + 56BA1E802BAB2E43001CF69F /* ErrorPageTabExtensionTest.swift in Sources */, B603975029C1FF5F00902A34 /* TestsURLExtension.swift in Sources */, B6A5A27E25B9403E00AA7ADA /* FileStoreMock.swift in Sources */, 56534DED29DF252C00121467 /* CapturingDefaultBrowserProvider.swift in Sources */, @@ -14496,7 +14540,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 134.0.0; + version = 134.0.1; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 403ec8030a..48d2d9aea8 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "bc70d1a27263cc97a4060ac9e73ec10929c28a29", - "version" : "134.0.0" + "revision" : "b0749d25996c0fa18be07b7851f02ebb3b9fab50", + "version" : "134.0.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "62d5dc3d02f6a8347dc5f0b52162a0107d38b74c", - "version" : "5.8.0" + "revision" : "1bb3bc5eb565735051f342a87b5405d4374876c7", + "version" : "5.12.0" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "620921fea14569eb00745cb5a44890d5890d99ec", - "version" : "3.4.0" + "revision" : "14b13d0c3db38f471ce4ba1ecb502ee1986c84d7", + "version" : "3.5.0" } }, { @@ -138,7 +138,7 @@ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/apple/swift-syntax", "state" : { "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", "version" : "509.1.1" diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme index aefd4b6b53..b70be830d9 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme @@ -69,6 +69,7 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" + language = "en" codeCoverageEnabled = "YES"> + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 36e9c3d90d..0337f4f9a6 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -215,10 +215,46 @@ struct UserText { static let tabPreferencesTitle = NSLocalizedString("tab.preferences.title", value: "Settings", comment: "Tab preferences title") static let tabBookmarksTitle = NSLocalizedString("tab.bookmarks.title", value: "Bookmarks", comment: "Tab bookmarks title") static let tabOnboardingTitle = NSLocalizedString("tab.onboarding.title", value: "Welcome", comment: "Tab onboarding title") + + // MARK: Error Pages static let tabErrorTitle = NSLocalizedString("tab.error.title", value: "Failed to open page", comment: "Tab error title") static let errorPageHeader = NSLocalizedString("page.error.header", value: "DuckDuckGo can’t load this page.", comment: "Error page heading text") static let webProcessCrashPageHeader = NSLocalizedString("page.crash.header", value: "This webpage has crashed.", comment: "Error page heading text shown when a Web Page process had crashed") static let webProcessCrashPageMessage = NSLocalizedString("page.crash.message", value: "Try reloading the page or come back later.", comment: "Error page message text shown when a Web Page process had crashed") + static let sslErrorPageHeader = NSLocalizedString("ssl.error.page.header", value: "Warning: This site may be insecure", comment: "Title shown in an error page that warn users of security risks on a website due to SSL issues") + static let sslErrorPageTabTitle = NSLocalizedString("ssl.error.page.tab.title", value: "Warning: Site May Be Insecure", comment: "Title shown in an error page tab that warn users of security risks on a website due to SSL issues") + static func sslErrorPageBody(_ domain: String) -> String { + let localized = NSLocalizedString("ssl.error.page.body", + value: "The certificate for this site is invalid. You might be connecting to a server that is pretending to be %1$@ which could put your confidential information at risk.", + comment: "Error description shown in an error page that warns users of security risks on a website due to SSL issues. %1$@ represent the site domain.") + return String(format: localized, domain) + } + static let sslErrorPageAdvancedButton = NSLocalizedString("ssl.error.page.advanced.button", value: "Advanced…", comment: "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to see advanced options on click.") + static let sslErrorPageLeaveSiteButton = NSLocalizedString("ssl.error.page.leave.site.button", value: "Leave This Site", comment: "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to leave the website and navigate to previous page.") + static let sslErrorPageVisitSiteButton = NSLocalizedString("ssl.error.page.visit.site.button", value: "Accept Risk and Visit Site", comment: "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to visit the website anyway despite the risks.") + static let sslErrorAdvancedInfoTitle = NSLocalizedString("ssl.error.page.advanced.info.title", value: "DuckDuckGo warns you when a website has an invalid certificate.", comment: "Title of the Advanced info section shown in an error page that warns users of security risks on a website due to SSL issues.") + static let sslErrorAdvancedInfoBodyWrongHost = NSLocalizedString("ssl.error.page.advanced.info.body.wrong.host", value: "It’s possible that the website is misconfigured or that an attacker has compromised your connection.", comment: "Body of the text of the Advanced info shown in an error page that warns users of security risks on a website due to SSL issues.") + static let sslErrorAdvancedInfoBodyExpired = NSLocalizedString("ssl.error.page.advanced.info.body.expired", value: "It’s possible that the website is misconfigured, that an attacker has compromised your connection, or that your system clock is incorrect.", comment: "Body of the text of the Advanced info shown in an error page that warns users of security risks on a website due to SSL issues.") + static func sslErrorCertificateExpiredMessage(_ domain: String) -> String { + let localized = NSLocalizedString("ssl.error.certificate.expired.message", + value: "The security certificate for %1$@ is expired.", + comment: "Describes an SSL error where a website's security certificate is expired. '%1$@' is a placeholder for the website's domain.") + return String(format: localized, domain) + } + static func sslErrorCertificateWrongHostMessage(_ domain: String, eTldPlus1: String) -> String { + let localized = NSLocalizedString("ssl.error.wrong.host.message", + value: "The security certificate for %1$@ does not match *.%2$@.", + comment: "Explains an SSL error when a site's certificate doesn't match its domain. '%1$@' is the site's domain.") + return String(format: localized, domain, eTldPlus1) + } + static func sslErrorCertificateSelfSignedMessage(_ domain: String) -> String { + let localized = NSLocalizedString("ssl.error.self.signed.message", + value: "The security certificate for %1$@ is not trusted by your device's operating system.", + comment: "Warns the user that the site's security certificate is self-signed and not trusted. '%1$@' is the site's domain.") + return String(format: localized, domain) + } + + static let openSystemPreferences = NSLocalizedString("open.preferences", value: "Open System Preferences", comment: "Open System Preferences (to re-enable permission for the App) (up to and including macOS 12") static let openSystemSettings = NSLocalizedString("open.settings", value: "Open System Settings…", comment: "This string represents a prompt or button label prompting the user to open system settings") diff --git a/DuckDuckGo/Common/Utilities/CertificateTrustEvaluator.swift b/DuckDuckGo/Common/Utilities/CertificateTrustEvaluator.swift new file mode 100644 index 0000000000..541f61c95d --- /dev/null +++ b/DuckDuckGo/Common/Utilities/CertificateTrustEvaluator.swift @@ -0,0 +1,32 @@ +// +// CertificateTrustEvaluator.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol CertificateTrustEvaluating { + func evaluateCertificateTrust(trust: SecTrust?) -> Bool? +} + +struct CertificateTrustEvaluator: CertificateTrustEvaluating { + func evaluateCertificateTrust(trust: SecTrust?) -> Bool? { + var error: CFError? + guard let trust else { return nil } + let result = SecTrustEvaluateWithError(trust, &error) + return result + } +} diff --git a/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift b/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift index af9d04103e..a288139546 100644 --- a/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift +++ b/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift @@ -19,6 +19,7 @@ import Foundation import ContentScopeScripts import WebKit +import Common struct ErrorPageHTMLTemplate { @@ -43,3 +44,130 @@ struct ErrorPageHTMLTemplate { } } + +struct SSLErrorPageHTMLTemplate { + let domain: String + let errorCode: Int + let tld = TLD() + + static var htmlTemplatePath: String { + guard let file = ContentScopeScripts.Bundle.path(forResource: "index", ofType: "html", inDirectory: "pages/sslerrorpage") else { + assertionFailure("HTML template not found") + return "" + } + return file + } + + func makeHTMLFromTemplate() -> String { + let sslError = SSLErrorType.forErrorCode(errorCode) + guard let html = try? String(contentsOfFile: Self.htmlTemplatePath) else { + assertionFailure("Should be able to load template") + return "" + } + let eTldPlus1 = tld.eTLDplus1(domain) ?? domain + let loadTimeData = createJSONString(header: sslError.header, body: sslError.body(for: domain), advancedButton: sslError.advancedButton, leaveSiteButton: sslError.leaveSiteButton, advancedInfoHeader: sslError.advancedInfoTitle, specificMessage: sslError.specificMessage(for: domain, eTldPlus1: eTldPlus1), advancedInfoBody: sslError.advancedInfoBody, visitSiteButton: sslError.visitSiteButton) + return html.replacingOccurrences(of: "$LOAD_TIME_DATA$", with: loadTimeData, options: .literal) + } + + private func createJSONString(header: String, body: String, advancedButton: String, leaveSiteButton: String, advancedInfoHeader: String, specificMessage: String, advancedInfoBody: String, visitSiteButton: String) -> String { + let innerDictionary: [String: Any] = [ + "header": header.escapedUnicodeHtmlString(), + "body": body.escapedUnicodeHtmlString(), + "advancedButton": advancedButton.escapedUnicodeHtmlString(), + "leaveSiteButton": leaveSiteButton.escapedUnicodeHtmlString(), + "advancedInfoHeader": advancedInfoHeader.escapedUnicodeHtmlString(), + "specificMessage": specificMessage.escapedUnicodeHtmlString(), + "advancedInfoBody": advancedInfoBody.escapedUnicodeHtmlString(), + "visitSiteButton": visitSiteButton.escapedUnicodeHtmlString() + ] + + let outerDictionary: [String: Any] = [ + "strings": innerDictionary + ] + + do { + let jsonData = try JSONSerialization.data(withJSONObject: outerDictionary, options: .prettyPrinted) + if let jsonString = String(data: jsonData, encoding: .utf8) { + return jsonString + } else { + return "Error: Could not encode jsonData to String." + } + } catch { + return "Error: \(error.localizedDescription)" + } + } + +} + +public enum SSLErrorType { + case expired + case wrongHost + case selfSigned + case invalid + + var header: String { + return UserText.sslErrorPageHeader + } + + func body(for domain: String) -> String { + let boldDomain = "\(domain)" + return UserText.sslErrorPageBody(boldDomain) + } + + var advancedButton: String { + return UserText.sslErrorPageAdvancedButton + } + + var leaveSiteButton: String { + return UserText.sslErrorPageLeaveSiteButton + } + + var visitSiteButton: String { + return UserText.sslErrorPageVisitSiteButton + } + + var advancedInfoTitle: String { + return UserText.sslErrorAdvancedInfoTitle + } + + var advancedInfoBody: String { + switch self { + case .expired: + return UserText.sslErrorAdvancedInfoBodyExpired + case .wrongHost: + return UserText.sslErrorAdvancedInfoBodyWrongHost + case .selfSigned: + return UserText.sslErrorAdvancedInfoBodyWrongHost + case .invalid: + return UserText.sslErrorAdvancedInfoBodyWrongHost + } + } + + func specificMessage(for domain: String, eTldPlus1: String) -> String { + let boldDomain = "\(domain)" + let boldETldPlus1 = "\(eTldPlus1)" + switch self { + case .expired: + return UserText.sslErrorCertificateExpiredMessage(boldDomain) + case .wrongHost: + return UserText.sslErrorCertificateWrongHostMessage(boldDomain, eTldPlus1: boldETldPlus1) + case .selfSigned: + return UserText.sslErrorCertificateSelfSignedMessage(boldDomain) + case .invalid: + return UserText.sslErrorCertificateSelfSignedMessage(boldDomain) + } + } + + static func forErrorCode(_ errorCode: Int) -> Self { + switch Int32(errorCode) { + case errSSLCertExpired: + return .expired + case errSSLHostNameMismatch: + return .wrongHost + case errSSLXCertChainInvalid: + return .selfSigned + default: + return .invalid + } + } +} diff --git a/DuckDuckGo/ErrorPage/SSLErrorPageUserScript.swift b/DuckDuckGo/ErrorPage/SSLErrorPageUserScript.swift new file mode 100644 index 0000000000..1f3487834e --- /dev/null +++ b/DuckDuckGo/ErrorPage/SSLErrorPageUserScript.swift @@ -0,0 +1,77 @@ +// +// SSLErrorPageUserScript.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UserScript + +final class SSLErrorPageUserScript: NSObject, Subfeature { + enum MessageName: String, CaseIterable { + case leaveSite + case visitSite + } + + public let messageOriginPolicy: MessageOriginPolicy = .all + public let featureName: String = "sslErrorPage" + + var isEnabled: Bool = false + var failingURL: URL? + + weak var broker: UserScriptMessageBroker? + weak var delegate: SSLErrorPageUserScriptDelegate? + + func with(broker: UserScriptMessageBroker) { + self.broker = broker + } + + @MainActor + func handler(forMethodNamed methodName: String) -> Subfeature.Handler? { + guard isEnabled else { return nil } + switch MessageName(rawValue: methodName) { + case .leaveSite: + return handleLeaveSiteAction + case .visitSite: + return handleVisitSiteAction + default: + assertionFailure("SSLErrorPageUserScript: Failed to parse User Script message: \(methodName)") + return nil + } + } + + @MainActor + func handleLeaveSiteAction(params: Any, message: UserScriptMessage) -> Encodable? { + delegate?.leaveSite() + return nil + } + + @MainActor + func handleVisitSiteAction(params: Any, message: UserScriptMessage) -> Encodable? { + delegate?.visitSite() + return nil + } + + // MARK: - UserValuesNotification + + struct UserValuesNotification: Encodable { + let userValuesNotification: UserValues + } +} + +protocol SSLErrorPageUserScriptDelegate: AnyObject { + func leaveSite() + func visitSite() +} diff --git a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift index 8b784addef..32c34a8b9b 100644 --- a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift +++ b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift @@ -21,6 +21,7 @@ import BrowserServicesKit public enum FeatureFlag: String { case debugMenu + case sslCertificatesBypass /// Add experimental atb parameter to SERP queries for internal users to display Privacy Reminder /// https://app.asana.com/0/1199230911884351/1205979030848528/f @@ -34,6 +35,8 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .internalOnly case .appendAtbToSerpQueries: return .internalOnly + case .sslCertificatesBypass: + return .remoteReleasable(.subfeature(sslCertificatesSubfeature.allowBypass)) } } } diff --git a/DuckDuckGo/InfoPlist.xcstrings b/DuckDuckGo/InfoPlist.xcstrings index e4f7972ce7..28e181ef08 100644 --- a/DuckDuckGo/InfoPlist.xcstrings +++ b/DuckDuckGo/InfoPlist.xcstrings @@ -363,4 +363,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 04f8d61b80..578c6d0b12 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -769,6 +769,7 @@ }, "Add Folder" : { "comment" : "Add Folder popover: Create folder button", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -1115,6 +1116,7 @@ }, "Address:" : { "comment" : "Add Bookmark dialog bookmark url field heading", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -7272,6 +7274,7 @@ }, "Bookmark Added" : { "comment" : "Bookmark Added popover title", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -9042,7 +9045,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Lesezeichen hinzufügen" + "value" : "Add Bookmark" } }, "en" : { @@ -9054,43 +9057,43 @@ "es" : { "stringUnit" : { "state" : "translated", - "value" : "Añadir marcador" + "value" : "Add Bookmark" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ajouter un signet" + "value" : "Add Bookmark" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Aggiungi ai segnalibri" + "value" : "Add Bookmark" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bladwijzer toevoegen" + "value" : "Add Bookmark" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dodaj zakładkę" + "value" : "Add Bookmark" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Adicionar marcador" + "value" : "Add Bookmark" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Добавить закладку" + "value" : "Add Bookmark" } } } @@ -9162,7 +9165,7 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Lesezeichen bearbeiten" + "value" : "Edit Bookmark" } }, "en" : { @@ -9174,43 +9177,43 @@ "es" : { "stringUnit" : { "state" : "translated", - "value" : "Editar marcador" + "value" : "Edit Bookmark" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Modifier le signet" + "value" : "Edit Bookmark" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Modifica segnalibro" + "value" : "Edit Bookmark" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Bladwijzer bewerken" + "value" : "Edit Bookmark" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Edytuj zakładkę" + "value" : "Edit Bookmark" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Editar marcador" + "value" : "Edit Bookmark" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Редактировать закладку" + "value" : "Edit Bookmark" } } } @@ -11289,6 +11292,7 @@ }, "Copy" : { "comment" : "Command", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -12528,6 +12532,7 @@ }, "Delete" : { "comment" : "Command", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -15724,6 +15729,7 @@ }, "Edit…" : { "comment" : "Command", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -30333,44 +30339,44 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Comparte tus ideas" + "state" : "translated", + "value" : "Sign Up To Participate" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Partagez votre avis" + "state" : "translated", + "value" : "Sign Up To Participate" } }, "it" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Comunicaci la tua opinione" + "state" : "translated", + "value" : "Sign Up To Participate" } }, "nl" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Deel je gedachten" + "state" : "translated", + "value" : "Sign Up To Participate" } }, "pl" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Podziel się przemyśleniami" + "state" : "translated", + "value" : "Sign Up To Participate" } }, "pt" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Partilha as tuas opiniões" + "state" : "translated", + "value" : "Sign Up To Participate" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Поделиться соображениями" + "state" : "translated", + "value" : "Sign Up To Participate" } } } @@ -30393,44 +30399,44 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Responde a nuestra breve encuesta y ayúdanos a crear el mejor navegador." + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Répondez à notre courte enquête et aidez-nous à créer le meilleur navigateur." + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "it" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Rispondi al nostro breve sondaggio e aiutaci a creare il browser migliore." + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "nl" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Vul onze korte enquête in en help ons de beste browser te bouwen." + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "pl" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Weź udział w krótkiej ankiecie i pomóż nam opracować najlepszą przeglądarkę." + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "pt" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Responde ao nosso curto inquérito e ajuda-nos a criar o melhor navegador." + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Пройдите короткий опрос и помогите DuckDuckGo стать лучшим из браузеров." + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." } } } @@ -30453,44 +30459,44 @@ }, "es" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Cuéntanos qué te ha traído hasta aquí" + "state" : "translated", + "value" : "Share Your Thoughts With Us" } }, "fr" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Dites-nous ce qui vous amène ici" + "state" : "translated", + "value" : "Share Your Thoughts With Us" } }, "it" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Raccontaci cosa ti ha portato qui" + "state" : "translated", + "value" : "Share Your Thoughts With Us" } }, "nl" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Vertel ons wat je hier heeft gebracht" + "state" : "translated", + "value" : "Share Your Thoughts With Us" } }, "pl" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Powiedz nam, co Cię tu sprowadziło" + "state" : "translated", + "value" : "Share Your Thoughts With Us" } }, "pt" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Diz-nos o que te trouxe aqui" + "state" : "translated", + "value" : "Share Your Thoughts With Us" } }, "ru" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Что привело вас к нам" + "state" : "translated", + "value" : "Share Your Thoughts With Us" } } } @@ -30690,6 +30696,48 @@ "state" : "new", "value" : "Sign Up To Participate" } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign Up To Participate" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign Up To Participate" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign Up To Participate" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign Up To Participate" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign Up To Participate" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign Up To Participate" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sign Up To Participate" + } } } }, @@ -30697,11 +30745,59 @@ "comment" : "Summary of the card on the new tab page that invites users to partecipate to a survey", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Join an interview with a member of our research team to help us build the best browser." } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join an interview with a member of our research team to help us build the best browser." + } } } }, @@ -30709,6 +30805,12 @@ "comment" : "Title of the Day 14 durvey of the Set Up section in the home page", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Share Your Thoughts With Us" + } + }, "en" : { "stringUnit" : { "state" : "new", @@ -31001,7 +31103,7 @@ }, "no.access.to.selected.folder" : { "comment" : "Alert presented to user if the app doesn't have rights to access selected folder", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -31061,7 +31163,7 @@ }, "no.access.to.selected.folder.header" : { "comment" : "Header of the alert dialog informing user about failed download", - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -45885,6 +45987,7 @@ }, "Remove" : { "comment" : "Remove bookmark button title", + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -48661,6 +48764,726 @@ } } }, + "ssl.error.certificate.expired.message" : { + "comment" : "Describes an SSL error where a website's security certificate is expired. '%1$@' is a placeholder for the website's domain.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das Sicherheitszertifikat für %1$@ ist abgelaufen." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The security certificate for %1$@ is expired." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El certificado de seguridad de %1$@ ha caducado." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le certificat de sécurité de %1$@ a expiré." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il certificato di sicurezza per %1$@ è scaduto." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Het beveiligingscertificaat voor %1$@ is verlopen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Certyfikat zabezpieczeń dla %1$@ wygasł." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O certificado de segurança de %1$@ expirou." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Срок действия сертификата безопасности сайта %1$@ истек." + } + } + } + }, + "ssl.error.page.advanced.button" : { + "comment" : "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to see advanced options on click.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erweitert …" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Advanced…" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avanzadas..." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Options avancées" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avanzate…" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geavanceerd ..." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaawansowane..." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avançado…" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дополнительно..." + } + } + } + }, + "ssl.error.page.advanced.info.body.expired" : { + "comment" : "Body of the text of the Advanced info shown in an error page that warns users of security risks on a website due to SSL issues.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es ist möglich, dass die Website falsch konfiguriert ist, ein Angreifer deine Verbindung kompromittiert hat oder deine Systemuhr falsch ist." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "It’s possible that the website is misconfigured, that an attacker has compromised your connection, or that your system clock is incorrect." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es posible que el sitio web esté mal configurado, que un atacante haya comprometido tu conexión o que el reloj del sistema no sea correcto." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il est possible que le site soit mal configuré, qu'un pirate ait compromis votre connexion ou que l'horloge de votre système soit incorrecte." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "È possibile che il sito web non sia configurato correttamente, che un aggressore abbia compromesso la tua connessione o che l'orologio del tuo sistema non sia corretto." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mogelijk is de website verkeerd geconfigureerd, heeft een aanvaller je verbinding gecompromitteerd of staat de klok van je systeem niet goed." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Możliwe, że witryna jest błędnie skonfigurowana, osoba atakująca naruszyła połączenie lub ustawienie zegara systemowego jest nieprawidłowe." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "É possível que o site esteja mal configurado, que um invasor tenha comprometido a tua ligação ou que o teu relógio do sistema esteja incorreto." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Возможно, неверно задана конфигурация сайта, соединение перехвачено злоумышленником либо неправильно настроены системные часы." + } + } + } + }, + "ssl.error.page.advanced.info.body.wrong.host" : { + "comment" : "Body of the text of the Advanced info shown in an error page that warns users of security risks on a website due to SSL issues.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Möglicherweise ist die Website falsch konfiguriert oder ein Angreifer hat deine Verbindung kompromittiert." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "It’s possible that the website is misconfigured or that an attacker has compromised your connection." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Es posible que el sitio web esté mal configurado o que un atacante haya comprometido tu conexión." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il est possible que le site soit mal configuré ou qu'un pirate ait compromis votre connexion." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "È possibile che il sito web non sia configurato correttamente o che un malintenzionato abbia compromesso la tua connessione." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mogelijk is de website verkeerd geconfigureerd of heeft een aanvaller je verbinding gecompromitteerd." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Możliwe, że witryna jest błędnie skonfigurowana lub osoba atakująca naruszyła połączenie." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "É possível que o site esteja mal configurado ou que um invasor tenha comprometido a tua ligação." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Возможно, неверно задана конфигурация сайта либо ваше соединение перехвачено злоумышленником." + } + } + } + }, + "ssl.error.page.advanced.info.title" : { + "comment" : "Title of the Advanced info section shown in an error page that warns users of security risks on a website due to SSL issues.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo warnt dich, wenn eine Website ein ungültiges Zertifikat hat." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo warns you when a website has an invalid certificate." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo te avisa cuando un sitio web tiene un certificado no válido." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo vous avertit lorsque le certificat d'un site Web n'est pas valide." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo ti avverte quando un sito web ha un certificato non valido." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo waarschuwt je wanneer een website een ongeldig certificaat heeft." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo ostrzega gdy witryna internetowa ma nieprawidłowy certyfikat." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O DuckDuckGo avisa-te quando um site tem um certificado inválido." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "DuckDuckGo предупредит вас, если у сайта окажется недействительный сертификат." + } + } + } + }, + "ssl.error.page.body" : { + "comment" : "Error description shown in an error page that warns users of security risks on a website due to SSL issues. %1$@ represent the site domain.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das Zertifikat für diese Website ist ungültig. Möglicherweise stellst du eine Verbindung zu einem Server her, der vorgibt, %1$@ zu sein, wodurch deine vertraulichen Daten in Gefahr sein könnten." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The certificate for this site is invalid. You might be connecting to a server that is pretending to be %1$@ which could put your confidential information at risk." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El certificado de este sitio no es válido. Puede que te estés conectando a un servidor que se hace pasar por %1$@, lo que podría poner en riesgo tu información confidencial." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le certificat de ce site n'est pas valide. Vous vous connectez peut-être à un serveur qui se fait passer pour %1$@, ce qui pourrait mettre vos informations confidentielles en danger." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il certificato di questo sito non è valido. Potresti collegarti a un server che finge di essere %1$@ e che potrebbe mettere a rischio le tue informazioni riservate." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Het certificaat voor deze website is ongeldig. Mogelijk maak je verbinding met een server die zich voordoet als %1$@, waardoor je vertrouwelijke informatie in gevaar kan komen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Certyfikat tej witryny jest nieprawidłowy. Być może łączysz się z serwerem podszywającym się pod %1$@, co może narazić poufne informacje na niebezpieczeństwo." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O certificado deste site é inválido. Podes estar a estabelecer ligação a um servidor que finge ser %1$@, o que pode colocar as tuas informações confidenciais em risco." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сертификат этого сайта недействителен. Возможно, вы пытаетесь подключиться к серверу, который выдает себя за %1$@, что ставит под угрозу вашу конфиденциальную информацию." + } + } + } + }, + "ssl.error.page.header" : { + "comment" : "Title shown in an error page that warn users of security risks on a website due to SSL issues", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Warnung: Diese Website ist möglicherweise unsicher" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Warning: This site may be insecure" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advertencia: este sitio puede ser inseguro" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avertissement : ce site n'est peut-être pas sécurisé" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Attenzione: questo sito potrebbe non essere sicuro" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Waarschuwing: Deze website is mogelijk onveilig" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ostrzeżenie: ta witryna może być niebezpieczna" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aviso: este site pode ser inseguro" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Внимание! Возможно, сайт небезопасен" + } + } + } + }, + "ssl.error.page.leave.site.button" : { + "comment" : "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to leave the website and navigate to previous page.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diese Website verlassen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Leave This Site" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salir de este sitio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quitter ce site" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esci da questo sito" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deze website verlaten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opuść tę stronę" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deixar este site" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Покинуть сайт" + } + } + } + }, + "ssl.error.page.tab.title" : { + "comment" : "Title shown in an error page tab that warn users of security risks on a website due to SSL issues", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Warnung: Website ist möglicherweise unsicher" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Warning: Site May Be Insecure" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advertencia: el sitio puede ser inseguro" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avertissement : le site n'est peut-être pas sécurisé" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Attenzione: il sito potrebbe non essere sicuro" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Waarschuwing: Website mogelijk onveilig" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ostrzeżenie: witryna może być niebezpieczna" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aviso: o site pode ser inseguro" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Внимание! Возможно, сайт небезопасен" + } + } + } + }, + "ssl.error.page.visit.site.button" : { + "comment" : "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to visit the website anyway despite the risks.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Risiko akzeptieren und Website besuchen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Accept Risk and Visit Site" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aceptar el riesgo y visitar el sitio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accepter le risque et visiter le site" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accetta il rischio e visita il sito" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Risico accepteren en site bezoeken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaakceptuj ryzyko i odwiedź witrynę" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aceitar o risco e visitar o site" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Принять риск и перейти на сайт" + } + } + } + }, + "ssl.error.self.signed.message" : { + "comment" : "Warns the user that the site's security certificate is self-signed and not trusted. '%1$@' is the site's domain.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das Sicherheitszertifikat für %1$@ wird vom Betriebssystem deines Geräts als nicht vertrauenswürdig eingestuft." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The security certificate for %1$@ is not trusted by your device's operating system." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El sistema operativo del dispositivo no confía en el certificado de seguridad de %1$@." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le certificat de sécurité de %1$@ n'est pas approuvé par le système d'exploitation de votre appareil." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il certificato di sicurezza di %1$@ non è considerato attendibile dal sistema operativo del tuo dispositivo." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Het besturingssysteem van je apparaat vertrouwt het beveiligingscertificaat voor %1$@ niet." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Certyfikat bezpieczeństwa %1$@ nie jest zaufany przez system operacyjny urządzenia." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O sistema operativo do teu dispositivo não confia no certificado de segurança de %1$@." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Операционная система вашего устройства не доверяет сертификату безопасности сайта %1$@." + } + } + } + }, + "ssl.error.wrong.host.message" : { + "comment" : "Explains an SSL error when a site's certificate doesn't match its domain. '%1$@' is the site's domain.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das Sicherheitszertifikat für %1$@ stimmt nicht mit *.%2$@ überein." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The security certificate for %1$@ does not match *.%2$@." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El certificado de seguridad de %1$@ no coincide con *.%2$@." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le certificat de sécurité de %1$@ ne correspond pas à *.%2$@." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il certificato di protezione per %1$@ non corrisponde a *.%2$@." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Het beveiligingscertificaat voor %1$@ komt niet overeen met *.%2$@. " + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Certyfikat zabezpieczeń %1$@ nie jest zgodny z *.%2$@." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O certificado de segurança de %1$@ não corresponde a *.%2$@." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сертификат безопасности сайта %1$@ не подходит для домена *.%2$@." + } + } + } + }, "Start Speaking" : { "comment" : "Main Menu Edit-Speech item", "localizations" : { @@ -52312,4 +53135,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 148900fcec..e17e162d79 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -799,11 +799,12 @@ final class AddressBarButtonsViewController: NSViewController { guard let host = url.host else { break } let isNotSecure = url.scheme == URL.NavigationalScheme.http.rawValue + let isCertificateValid = tabViewModel.tab.isCertificateValid ?? true let configuration = ContentBlocking.shared.privacyConfigurationManager.privacyConfig let isUnprotected = configuration.isUserUnprotected(domain: host) - let isShieldDotVisible = isNotSecure || isUnprotected + let isShieldDotVisible = isNotSecure || isUnprotected || !isCertificateValid privacyEntryPointButton.image = isShieldDotVisible ? .shieldDot : .shield diff --git a/DuckDuckGo/Tab/Model/Tab+Navigation.swift b/DuckDuckGo/Tab/Model/Tab+Navigation.swift index 6ada06456b..48eedb91f4 100644 --- a/DuckDuckGo/Tab/Model/Tab+Navigation.swift +++ b/DuckDuckGo/Tab/Model/Tab+Navigation.swift @@ -85,6 +85,9 @@ extension Tab: NavigationResponder { // Tab Snapshots .weak(nullable: self.tabSnapshots), + // Error Page + .weak(nullable: self.errorPage), + // should be the last, for Unit Tests navigation events tracking .struct(nullable: testsClosureNavigationResponder) ) diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index bac1e48b47..a1e4228051 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -340,6 +340,8 @@ protocol NewWindowPolicyDecisionMaker { private let statisticsLoader: StatisticsLoader? private let internalUserDecider: InternalUserDecider? let pinnedTabsManager: PinnedTabsManager + private let certificateTrustEvaluator: CertificateTrustEvaluating + var isCertificateValid: Bool? private(set) var tunnelController: NetworkProtectionIPCTunnelController @@ -382,7 +384,8 @@ protocol NewWindowPolicyDecisionMaker { canBeClosedWithBack: Bool = false, lastSelectedAt: Date? = nil, webViewSize: CGSize = CGSize(width: 1024, height: 768), - startupPreferences: StartupPreferences = StartupPreferences.shared + startupPreferences: StartupPreferences = StartupPreferences.shared, + certificateTrustEvaluator: CertificateTrustEvaluating = CertificateTrustEvaluator() ) { let duckPlayer = duckPlayer @@ -421,7 +424,8 @@ protocol NewWindowPolicyDecisionMaker { canBeClosedWithBack: canBeClosedWithBack, lastSelectedAt: lastSelectedAt, webViewSize: webViewSize, - startupPreferences: startupPreferences) + startupPreferences: startupPreferences, + certificateTrustEvaluator: certificateTrustEvaluator) } @MainActor @@ -451,7 +455,8 @@ protocol NewWindowPolicyDecisionMaker { canBeClosedWithBack: Bool, lastSelectedAt: Date?, webViewSize: CGSize, - startupPreferences: StartupPreferences + startupPreferences: StartupPreferences, + certificateTrustEvaluator: CertificateTrustEvaluating ) { self.content = content @@ -467,6 +472,7 @@ protocol NewWindowPolicyDecisionMaker { self.interactionState = interactionStateData.map(InteractionState.loadCachedFromTabContent) ?? .none self.lastSelectedAt = lastSelectedAt self.startupPreferences = startupPreferences + self.certificateTrustEvaluator = certificateTrustEvaluator let configuration = webViewConfiguration ?? WKWebViewConfiguration() configuration.applyStandardConfiguration(contentBlocking: privacyFeatures.contentBlocking, @@ -1219,7 +1225,13 @@ protocol NewWindowPolicyDecisionMaker { webView.publisher(for: \.serverTrust) .sink { [weak self] serverTrust in - self?.privacyInfo?.serverTrust = serverTrust + guard let self else { return } + self.isCertificateValid = self.certificateTrustEvaluator.evaluateCertificateTrust(trust: serverTrust) + if self.isCertificateValid == true { + self.privacyInfo?.serverTrust = serverTrust + } else { + self.privacyInfo?.serverTrust = nil + } } .store(in: &webViewCancellables) @@ -1478,10 +1490,10 @@ extension Tab/*: NavigationResponder*/ { // to be moved to Tab+Navigation.swift invalidateInteractionStateData() - if !error.isFrameLoadInterrupted, !error.isNavigationCancelled, - // don‘t show an error page if the error was already handled - // (by SearchNonexistentDomainNavigationResponder) or another navigation was triggered by `setContent` - self.content.urlForWebView == url { + if !error.isFrameLoadInterrupted, !error.isNavigationCancelled, error.errorCode != NSURLErrorServerCertificateUntrusted, + // don‘t show an error page if the error was already handled + // (by SearchNonexistentDomainNavigationResponder) or another navigation was triggered by `setContent` + self.content.urlForWebView == url { self.error = error // when already displaying the error page and reload navigation fails again: don‘t navigate, just update page HTML diff --git a/DuckDuckGo/Tab/TabExtensions/SSLErrorPageTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/SSLErrorPageTabExtension.swift new file mode 100644 index 0000000000..aac116210a --- /dev/null +++ b/DuckDuckGo/Tab/TabExtensions/SSLErrorPageTabExtension.swift @@ -0,0 +1,180 @@ +// +// SSLErrorPageTabExtension.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Navigation +import WebKit +import Combine +import ContentScopeScripts +import BrowserServicesKit + +protocol SSLErrorPageScriptProvider { + var sslErrorPageUserScript: SSLErrorPageUserScript? { get } +} + +extension UserScripts: SSLErrorPageScriptProvider {} + +final class SSLErrorPageTabExtension { + weak var webView: ErrorPageTabExtensionNavigationDelegate? + private weak var sslErrorPageUserScript: SSLErrorPageUserScript? + private var shouldBypassSSLError = false + private var urlCredentialCreator: URLCredentialCreating + private var featureFlagger: FeatureFlagger + + private var cancellables = Set() + + init( + webViewPublisher: some Publisher, + scriptsPublisher: some Publisher, + urlCredentialCreator: URLCredentialCreating = URLCredentialCreator(), + featureFlagger: FeatureFlagger = NSApp.delegateTyped.featureFlagger) { + self.featureFlagger = featureFlagger + self.urlCredentialCreator = urlCredentialCreator + webViewPublisher.sink { [weak self] webView in + self?.webView = webView + }.store(in: &cancellables) + scriptsPublisher.sink { [weak self] scripts in + self?.sslErrorPageUserScript = scripts.sslErrorPageUserScript + self?.sslErrorPageUserScript?.delegate = self + }.store(in: &cancellables) + } + + @MainActor + private func loadSSLErrorHTML(url: URL, alternate: Bool, errorCode: Int) { + let domain: String = url.host ?? url.toString(decodePunycode: true, dropScheme: true, dropTrailingSlash: true) + let html = SSLErrorPageHTMLTemplate(domain: domain, errorCode: errorCode).makeHTMLFromTemplate() + webView?.loadAlternateHTML(html, baseURL: .error, forUnreachableURL: url) + loadHTML(html: html, url: url, alternate: alternate) + } + + @MainActor + private func loadErrorHTML(_ error: WKError, header: String, forUnreachableURL url: URL, alternate: Bool) { + let html = ErrorPageHTMLTemplate(error: error, header: header).makeHTMLFromTemplate() + loadHTML(html: html, url: url, alternate: alternate) + } + + @MainActor + private func loadHTML(html: String, url: URL, alternate: Bool) { + if alternate { + webView?.loadAlternateHTML(html, baseURL: .error, forUnreachableURL: url) + } else { + webView?.setDocumentHtml(html) + } + } + +} + +extension SSLErrorPageTabExtension: NavigationResponder { + @MainActor + func navigation(_ navigation: Navigation, didFailWith error: WKError) { + let url = error.failingUrl ?? navigation.url + guard navigation.isCurrent else { return } + guard error.errorCode != NSURLErrorCannotFindHost else { return } + + if !error.isFrameLoadInterrupted, !error.isNavigationCancelled { + // when already displaying the error page and reload navigation fails again: don‘t navigate, just update page HTML + guard let webView else { return } + let shouldPerformAlternateNavigation = navigation.url != webView.url || navigation.navigationAction.targetFrame?.url != .error + if featureFlagger.isFeatureOn(.sslCertificatesBypass), + error.errorCode == NSURLErrorServerCertificateUntrusted, + let errorCode = error.userInfo["_kCFStreamErrorCodeKey"] as? Int { + sslErrorPageUserScript?.failingURL = url + loadSSLErrorHTML(url: url, alternate: shouldPerformAlternateNavigation, errorCode: errorCode) + } + } + } + + @MainActor + func navigationDidFinish(_ navigation: Navigation) { + sslErrorPageUserScript?.isEnabled = navigation.url == sslErrorPageUserScript?.failingURL + } + + @MainActor + func didReceive(_ challenge: URLAuthenticationChallenge, for navigation: Navigation?) async -> AuthChallengeDisposition? { + guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else { return nil } + guard shouldBypassSSLError else { return nil} + guard navigation?.url == webView?.url else { return nil } + guard let credential = urlCredentialCreator.urlCredentialFrom(trust: challenge.protectionSpace.serverTrust) else { return nil } + + shouldBypassSSLError = false + return .credential(credential) + } +} + +extension SSLErrorPageTabExtension: SSLErrorPageUserScriptDelegate { + func leaveSite() { + guard webView?.canGoBack == true else { + webView?.close() + return + } + _ = webView?.goBack() + } + + func visitSite() { + shouldBypassSSLError = true + _ = webView?.reloadPage() + } +} + +protocol ErrorPageTabExtensionProtocol: AnyObject, NavigationResponder {} + +extension SSLErrorPageTabExtension: TabExtension, ErrorPageTabExtensionProtocol { + typealias PublicProtocol = ErrorPageTabExtensionProtocol + func getPublicProtocol() -> PublicProtocol { self } +} + +extension TabExtensions { + var errorPage: ErrorPageTabExtensionProtocol? { + resolve(SSLErrorPageTabExtension.self) + } +} + +protocol ErrorPageTabExtensionNavigationDelegate: AnyObject { + var url: URL? { get } + var canGoBack: Bool { get } + func loadAlternateHTML(_ html: String, baseURL: URL, forUnreachableURL failingURL: URL) + func setDocumentHtml(_ html: String) + func goBack() -> WKNavigation? + func close() + func reloadPage() -> WKNavigation? +} + +extension ErrorPageTabExtensionNavigationDelegate { + func reloadPage() -> WKNavigation? { + guard let wevView = self as? WKWebView else { return nil } + if let item = wevView.backForwardList.currentItem { + return wevView.go(to: item) + } + return nil + } +} + +extension WKWebView: ErrorPageTabExtensionNavigationDelegate { } + +protocol URLCredentialCreating { + func urlCredentialFrom(trust: SecTrust?) -> URLCredential? +} + +struct URLCredentialCreator: URLCredentialCreating { + func urlCredentialFrom(trust: SecTrust?) -> URLCredential? { + if let trust { + return URLCredential(trust: trust) + } + return nil + } +} diff --git a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift index 56351d7c11..b56da3410d 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift @@ -185,6 +185,11 @@ extension TabExtensionsBuilder { scriptsPublisher: userScripts.compactMap { $0 }, webViewPublisher: args.webViewFuture) } + + add { + SSLErrorPageTabExtension(webViewPublisher: args.webViewFuture, + scriptsPublisher: userScripts.compactMap { $0 }) + } } } diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 017c0c6716..c34baac57b 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -46,6 +46,7 @@ final class UserScripts: UserScriptsProvider { let autoconsentUserScript: UserScriptWithAutoconsent let youtubeOverlayScript: YoutubeOverlayUserScript? let youtubePlayerUserScript: YoutubePlayerUserScript? + let sslErrorPageUserScript: SSLErrorPageUserScript? init(with sourceProvider: ScriptSourceProviding) { clickToLoadScript = ClickToLoadUserScript(scriptSourceProvider: sourceProvider) @@ -64,14 +65,16 @@ final class UserScripts: UserScriptsProvider { autoconsentUserScript = AutoconsentUserScript(scriptSource: sourceProvider, config: sourceProvider.privacyConfigurationManager.privacyConfig) + sslErrorPageUserScript = SSLErrorPageUserScript() + + specialPages = SpecialPagesUserScript() + if DuckPlayer.shared.isAvailable { youtubeOverlayScript = YoutubeOverlayUserScript() youtubePlayerUserScript = YoutubePlayerUserScript() - specialPages = SpecialPagesUserScript() } else { youtubeOverlayScript = nil youtubePlayerUserScript = nil - specialPages = nil } userScripts.append(autoconsentUserScript) @@ -80,11 +83,14 @@ final class UserScripts: UserScriptsProvider { contentScopeUserScriptIsolated.registerSubfeature(delegate: youtubeOverlayScript) } - if let youtubePlayerUserScript { - if let specialPages = specialPages { + if let specialPages = specialPages { + if let sslErrorPageUserScript { + specialPages.registerSubfeature(delegate: sslErrorPageUserScript) + } + if let youtubePlayerUserScript { specialPages.registerSubfeature(delegate: youtubePlayerUserScript) - userScripts.append(specialPages) } + userScripts.append(specialPages) } #if SUBSCRIPTION diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index c6853be3fb..5c03b57c92 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -20,6 +20,7 @@ import BrowserServicesKit import Cocoa import Combine import Common +import WebKit final class TabViewModel { @@ -203,6 +204,7 @@ final class TabViewModel { .sink { [weak self] _ in self?.updateTitle() self?.updateFavicon() + self?.updateCanBeBookmarked() }.store(in: &cancellables) } @@ -293,7 +295,11 @@ final class TabViewModel { switch tab.content { // keep an old tab title for web page terminated page, display "Failed to open page" for loading errors case _ where isShowingErrorPage && (tab.error?.code != .webContentProcessTerminated || tab.title == nil): - title = UserText.tabErrorTitle + if tab.error?.errorCode == NSURLErrorServerCertificateUntrusted { + title = UserText.sslErrorPageTabTitle + } else { + title = UserText.tabErrorTitle + } case .dataBrokerProtection: title = UserText.tabDataBrokerProtectionTitle case .settings: @@ -326,10 +332,9 @@ final class TabViewModel { private func updateFavicon(_ tabFavicon: NSImage?? = .none /* provided from .sink or taken from tab.favicon (optional) if .none */) { guard !isShowingErrorPage else { - favicon = .alertCircleColor16 + favicon = errorFaviconToShow(error: tab.error) return } - switch tab.content { case .dataBrokerProtection: favicon = Favicon.dataBrokerProtection @@ -368,6 +373,13 @@ final class TabViewModel { updateAddressBarStrings() } + private func errorFaviconToShow(error: WKError?) -> NSImage { + if error?.errorCode == NSURLErrorServerCertificateUntrusted { + return .redAlertCircle16 + } + return.alertCircleColor16 + } + // MARK: - Privacy icon animation let trackersAnimationTriggerPublisher = PassthroughSubject() diff --git a/IntegrationTests/Tab/AddressBarTests.swift b/IntegrationTests/Tab/AddressBarTests.swift index 22b6ce65aa..99dce527a4 100644 --- a/IntegrationTests/Tab/AddressBarTests.swift +++ b/IntegrationTests/Tab/AddressBarTests.swift @@ -780,4 +780,96 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(window2.firstResponder, window2) } + func test_WhenSiteCertificateNil_ThenAddressBarShowsStandardShieldIcon() async throws { + // GIVEN + let expectedImage = NSImage(named: "Shield")! + let evaluator = MockCertificateEvaluator() + let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .userEntered("")), certificateTrustEvaluator: evaluator) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + let tabLoadedPromise = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + // WHEN + window = WindowsManager.openNewWindow(with: viewModel)! + _=try await tabLoadedPromise.value + + // THEN + let shieldImage = mainViewController.navigationBarViewController.addressBarViewController!.addressBarButtonsViewController!.privacyEntryPointButton.image! + XCTAssertTrue(shieldImage.isEqualToImage(expectedImage)) + } + + func test_WhenSiteCertificateValid_ThenAddressBarShowsStandardShieldIcon() async throws { + // GIVEN + let expectedImage = NSImage(named: "Shield")! + let evaluator = MockCertificateEvaluator() + evaluator.isValidCertificate = true + let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .userEntered("")), certificateTrustEvaluator: evaluator) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + let tabLoadedPromise = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + // WHEN + window = WindowsManager.openNewWindow(with: viewModel)! + _=try await tabLoadedPromise.value + + // THEN + let shieldImage = mainViewController.navigationBarViewController.addressBarViewController!.addressBarButtonsViewController!.privacyEntryPointButton.image! + XCTAssertTrue(shieldImage.isEqualToImage(expectedImage)) + } + + func test_WhenSiteCertificateInvalid_ThenAddressBarShowsDottedShieldIcon() async throws { + // GIVEN + let expectedImage = NSImage(named: "ShieldDot")! + let evaluator = MockCertificateEvaluator() + evaluator.isValidCertificate = false + let tab = Tab(content: .url(.duckDuckGo, credential: nil, source: .userEntered("")), certificateTrustEvaluator: evaluator) + let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab])) + let tabLoadedPromise = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise() + + // WHEN + window = WindowsManager.openNewWindow(with: viewModel)! + _=try await tabLoadedPromise.value + + // THEN + let shieldImage = mainViewController.navigationBarViewController.addressBarViewController!.addressBarButtonsViewController!.privacyEntryPointButton.image! + XCTAssertTrue(shieldImage.isEqualToImage(expectedImage)) + } +} + +protocol MainActorPerformer { + func perform(_ closure: @MainActor () -> Void) +} +struct OnMainActor: MainActorPerformer { + private init() {} + + static func instance() -> MainActorPerformer { OnMainActor() } + + @MainActor(unsafe) + func perform(_ closure: @MainActor () -> Void) { + closure() + } +} + +extension NSImage { + func pngData() -> Data? { + guard let tiffRepresentation = self.tiffRepresentation, + let bitmapImage = NSBitmapImageRep(data: tiffRepresentation) else { + return nil + } + return bitmapImage.representation(using: .png, properties: [:]) + } + + func isEqualToImage(_ image: NSImage) -> Bool { + guard let data1 = self.pngData(), + let data2 = image.pngData() else { + return false + } + return data1 == data2 + } +} + +class MockCertificateEvaluator: CertificateTrustEvaluating { + var isValidCertificate: Bool? + + func evaluateCertificateTrust(trust: SecTrust?) -> Bool? { + return isValidCertificate + } } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 0cf9ebe7c0..0eb8a47757 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.0.1"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 7b5cbdc07d..f7a52283d8 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -31,7 +31,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.0.1"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions"), .package(path: "../LoginItems"), diff --git a/LocalPackages/Screenshot 2024-03-14 alle 13.19.29.png b/LocalPackages/Screenshot 2024-03-14 alle 13.19.29.png new file mode 100644 index 0000000000..7ddccad49d Binary files /dev/null and b/LocalPackages/Screenshot 2024-03-14 alle 13.19.29.png differ diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index d84099b47d..f50597c4ec 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "134.0.1"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings index a899610e5c..9ce1d2aa32 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings +++ b/LocalPackages/SyncUI/Sources/SyncUI/Resources/Localizable.xcstrings @@ -4743,11 +4743,59 @@ "comment" : "Alert message for multiple invalid bookmark being filtered out of synced data", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks (%d) can't sync because some of their fields exceed the character limit." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Some bookmarks (%d) can't sync because some of their fields exceed the character limit." } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks (%d) can't sync because some of their fields exceed the character limit." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks (%d) can't sync because some of their fields exceed the character limit." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks (%d) can't sync because some of their fields exceed the character limit." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks (%d) can't sync because some of their fields exceed the character limit." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks (%d) can't sync because some of their fields exceed the character limit." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks (%d) can't sync because some of their fields exceed the character limit." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks (%d) can't sync because some of their fields exceed the character limit." + } } } }, @@ -4755,11 +4803,59 @@ "comment" : "Alert message for 1 invalid bookmark being filtered out of synced data", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your bookmark for %@ can't sync because one of its fields exceeds the character limit." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Your bookmark for %@ can't sync because one of its fields exceeds the character limit." } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your bookmark for %@ can't sync because one of its fields exceeds the character limit." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your bookmark for %@ can't sync because one of its fields exceeds the character limit." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your bookmark for %@ can't sync because one of its fields exceeds the character limit." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your bookmark for %@ can't sync because one of its fields exceeds the character limit." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your bookmark for %@ can't sync because one of its fields exceeds the character limit." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your bookmark for %@ can't sync because one of its fields exceeds the character limit." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your bookmark for %@ can't sync because one of its fields exceeds the character limit." + } } } }, @@ -4767,11 +4863,59 @@ "comment" : "Alert title for invalid bookmarks being filtered out of synced data", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks are not syncing due to excessively long content in certain fields." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Some bookmarks are not syncing due to excessively long content in certain fields." } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks are not syncing due to excessively long content in certain fields." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks are not syncing due to excessively long content in certain fields." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks are not syncing due to excessively long content in certain fields." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks are not syncing due to excessively long content in certain fields." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks are not syncing due to excessively long content in certain fields." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks are not syncing due to excessively long content in certain fields." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some bookmarks are not syncing due to excessively long content in certain fields." + } } } }, @@ -4779,11 +4923,59 @@ "comment" : "Alert message for multiple invalid logins being filtered out of synced data", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some passwords (n) can't sync because some of their fields exceed the character limit." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Some passwords (n) can't sync because some of their fields exceed the character limit." } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some passwords (n) can't sync because some of their fields exceed the character limit." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some passwords (n) can't sync because some of their fields exceed the character limit." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some passwords (n) can't sync because some of their fields exceed the character limit." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some passwords (n) can't sync because some of their fields exceed the character limit." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some passwords (n) can't sync because some of their fields exceed the character limit." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some passwords (n) can't sync because some of their fields exceed the character limit." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some passwords (n) can't sync because some of their fields exceed the character limit." + } } } }, @@ -4791,11 +4983,59 @@ "comment" : "Alert message for 1 invalid login being filtered out of synced data", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your password for %@ can't sync because one of its fields exceeds the character limit." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Your password for %@ can't sync because one of its fields exceeds the character limit." } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your password for %@ can't sync because one of its fields exceeds the character limit." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your password for %@ can't sync because one of its fields exceeds the character limit." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your password for %@ can't sync because one of its fields exceeds the character limit." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your password for %@ can't sync because one of its fields exceeds the character limit." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your password for %@ can't sync because one of its fields exceeds the character limit." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your password for %@ can't sync because one of its fields exceeds the character limit." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your password for %@ can't sync because one of its fields exceeds the character limit." + } } } }, @@ -4803,11 +5043,59 @@ "comment" : "Alert title for invalid logins being filtered out of synced data", "extractionState" : "extracted_with_value", "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some logins are not syncing due to excessively long content in certain fields." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "Some logins are not syncing due to excessively long content in certain fields." } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some logins are not syncing due to excessively long content in certain fields." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some logins are not syncing due to excessively long content in certain fields." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some logins are not syncing due to excessively long content in certain fields." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some logins are not syncing due to excessively long content in certain fields." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some logins are not syncing due to excessively long content in certain fields." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some logins are not syncing due to excessively long content in certain fields." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Some logins are not syncing due to excessively long content in certain fields." + } } } }, diff --git a/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift b/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift new file mode 100644 index 0000000000..51cfbfff44 --- /dev/null +++ b/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift @@ -0,0 +1,411 @@ +// +// ErrorPageTabExtensionTest.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import BrowserServicesKit +import Combine +import Navigation +import Common +import WebKit +import XCTest + +@testable import DuckDuckGo_Privacy_Browser + +final class ErrorPageTabExtensionTest: XCTestCase { + + var mockWebViewPublisher: PassthroughSubject! + var scriptPublisher: PassthroughSubject! + var errorPageExtention: SSLErrorPageTabExtension! + var credentialCreator: MockCredentialCreator! + let errorURLString = "com.example.error" + + override func setUpWithError() throws { + mockWebViewPublisher = PassthroughSubject() + scriptPublisher = PassthroughSubject() + credentialCreator = MockCredentialCreator() + let featureFlagger = MockFeatureFlagger() + errorPageExtention = SSLErrorPageTabExtension(webViewPublisher: mockWebViewPublisher, scriptsPublisher: scriptPublisher, urlCredentialCreator: credentialCreator, featureFlagger: featureFlagger) + } + + override func tearDownWithError() throws { + mockWebViewPublisher = nil + scriptPublisher = nil + errorPageExtention = nil + credentialCreator = nil + } + + func testWhenWebViewPublisherPublishWebViewThenErrorPageExtensionHasCorrectWebView() throws { + // GIVEN + let aWebView = WKWebView() + + // WHEN + mockWebViewPublisher.send(aWebView) + + // THEN + XCTAssertTrue(errorPageExtention.webView === aWebView) + } + + @MainActor func testWhenCertificateExpired_ThenExpectedErrorPageIsShown() { + // GIVEN + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + let error = WKError(_nsError: NSError(domain: "com.example.error", code: NSURLErrorServerCertificateUntrusted, userInfo: ["_kCFStreamErrorCodeKey": -9814, "NSErrorFailingURLKey": URL(string: errorURLString)!])) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [], isCurrent: true, isCommitted: true) + let eTldPlus1 = TLD().eTLDplus1(errorURLString) ?? errorURLString + + // WHEN + errorPageExtention.navigation(navigation, didFailWith: error) + + // THEN + let expectedSpecificMessage = SSLErrorType.expired.specificMessage(for: errorURLString, eTldPlus1: eTldPlus1).replacingOccurrences(of: "", with: "<\\/b>").escapedUnicodeHtmlString() + XCTAssertTrue(mockWebView.capturedHTML.contains(expectedSpecificMessage)) + } + + @MainActor func testWhenCertificateSelfSigned_ThenExpectedErrorPageIsShown() { + // GIVEN + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + let error = WKError(_nsError: NSError(domain: "com.example.error", code: NSURLErrorServerCertificateUntrusted, userInfo: ["_kCFStreamErrorCodeKey": -9807, "NSErrorFailingURLKey": URL(string: errorURLString)!])) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [], isCurrent: true, isCommitted: true) + let eTldPlus1 = TLD().eTLDplus1(errorURLString) ?? errorURLString + + // WHEN + errorPageExtention.navigation(navigation, didFailWith: error) + + // THEN + let expectedSpecificMessage = SSLErrorType.selfSigned.specificMessage(for: errorURLString, eTldPlus1: eTldPlus1).replacingOccurrences(of: "", with: "<\\/b>").escapedUnicodeHtmlString() + XCTAssertTrue(mockWebView.capturedHTML.contains(expectedSpecificMessage)) + } + + @MainActor func testWhenCertificateWrongHost_ThenExpectedErrorPageIsShown() { + // GIVEN + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + let error = WKError(_nsError: NSError(domain: "com.example.error", code: NSURLErrorServerCertificateUntrusted, userInfo: ["_kCFStreamErrorCodeKey": -9843, "NSErrorFailingURLKey": URL(string: errorURLString)!])) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [], isCurrent: true, isCommitted: true) + let eTldPlus1 = TLD().eTLDplus1(errorURLString) ?? errorURLString + + // WHEN + errorPageExtention.navigation(navigation, didFailWith: error) + + // THEN + let expectedSpecificMessage = SSLErrorType.wrongHost.specificMessage(for: errorURLString, eTldPlus1: eTldPlus1).replacingOccurrences(of: "", with: "<\\/b>").escapedUnicodeHtmlString() + XCTAssertTrue(mockWebView.capturedHTML.contains(expectedSpecificMessage)) + + } + + @MainActor func test_WhenUserScriptsPublisherPublishSSLErrorPageScript_ThenErrorPageExtensionIsSetAsUserScriptDelegate() { + // GIVEN + let aSSLErrorUserScript = SSLErrorPageUserScript() + let mockScriptProvider = MockSSLErrorPageScriptProvider(script: aSSLErrorUserScript) + + // WHEN + scriptPublisher.send(mockScriptProvider) + + // THEN + XCTAssertNotNil(aSSLErrorUserScript.delegate) + } + + @MainActor func testWhenNavigationEnded_IfNoFailure_SSLUserScriptIsNotEnabled() { + // GIVEN + let userScript = SSLErrorPageUserScript() + let mockScriptProvider = MockSSLErrorPageScriptProvider(script: userScript) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.example.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + errorPageExtention.webView = mockWebView + scriptPublisher.send(mockScriptProvider) + + // WHEN + errorPageExtention.navigationDidFinish(navigation) + + // THEN + XCTAssertFalse(userScript.isEnabled) + XCTAssertNil(userScript.failingURL) + } + + @MainActor func testWhenNavigationEnded_IfNonSSLFailure_SSLUserScriptIsNotEnabled() { + // GIVEN + let userScript = SSLErrorPageUserScript() + let mockScriptProvider = MockSSLErrorPageScriptProvider(script: userScript) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.example.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + let errorDescription = "some error" + let error = WKError(_nsError: NSError(domain: "com.example.error", code: NSURLErrorUnknown, userInfo: ["_kCFStreamErrorCodeKey": -9843, "NSErrorFailingURLKey": URL(string: errorURLString)!, NSLocalizedDescriptionKey: errorDescription])) + errorPageExtention.webView = mockWebView + scriptPublisher.send(mockScriptProvider) + errorPageExtention.navigation(navigation, didFailWith: error) + + // WHEN + errorPageExtention.navigationDidFinish(navigation) + + // THEN + XCTAssertFalse(userScript.isEnabled) + XCTAssertNil(userScript.failingURL) + } + + @MainActor func testWhenNavigationEnded_IfSSLFailure_AndErrorURLIsDifferentFromNavigationURL_SSLUserScriptIsNotEnabled() { + // GIVEN + let userScript = SSLErrorPageUserScript() + let mockScriptProvider = MockSSLErrorPageScriptProvider(script: userScript) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.different.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + let error = WKError(_nsError: NSError(domain: "com.example.error", code: NSURLErrorServerCertificateUntrusted, userInfo: ["_kCFStreamErrorCodeKey": -9843, "NSErrorFailingURLKey": URL(string: errorURLString)!])) + errorPageExtention.webView = mockWebView + scriptPublisher.send(mockScriptProvider) + errorPageExtention.navigation(navigation, didFailWith: error) + + // WHEN + errorPageExtention.navigationDidFinish(navigation) + + // THEN + XCTAssertFalse(userScript.isEnabled) + XCTAssertEqual(userScript.failingURL?.absoluteString, errorURLString) + } + + @MainActor func testWhenNavigationEnded_IfSSLFailure_AndErrorURLIsTheSameAsNavigationURL_SSLUserScriptIsEnabled() { + // GIVEN + let userScript = SSLErrorPageUserScript() + let mockScriptProvider = MockSSLErrorPageScriptProvider(script: userScript) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.example.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + let error = WKError(_nsError: NSError(domain: "com.example.error", code: NSURLErrorServerCertificateUntrusted, userInfo: ["_kCFStreamErrorCodeKey": -9843, "NSErrorFailingURLKey": URL(string: errorURLString)!])) + errorPageExtention.webView = mockWebView + scriptPublisher.send(mockScriptProvider) + errorPageExtention.navigation(navigation, didFailWith: error) + + // WHEN + errorPageExtention.navigationDidFinish(navigation) + + // THEN + XCTAssertTrue(userScript.isEnabled) + XCTAssertEqual(userScript.failingURL?.absoluteString, errorURLString) + } + + func testWhenLeaveSiteCalled_AndCanGoBackTrue_ThenWebViewGoesBack() { + // GIVEN + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + + // WHEN + errorPageExtention.leaveSite() + + // THEN + XCTAssertTrue(mockWebView.goBackCalled) + } + + func testWhenLeaveSiteCalled_AndCanGoBackFalse_ThenWebViewCloses() { + // GIVEN + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + mockWebView.canGoBack = false + errorPageExtention.webView = mockWebView + + // WHEN + errorPageExtention.leaveSite() + + // THEN + XCTAssertTrue(mockWebView.closedCalled) + } + + func testWhenVisitSiteCalled_ThenWebViewReloads() { + // GIVEN + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + + // WHEN + errorPageExtention.visitSite() + + // THEN + XCTAssertTrue(mockWebView.reloadCalled) + } + + @MainActor + func testWhenDidReceiveChallange_IfChallangeForCertificateValidation_AndUserRequestBypass_AndNavigationURLIsTheSameAsWevViewURL_ThenReturnsCredentials() async { + // GIVEN + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.example.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + errorPageExtention.visitSite() + + // WHEN + var disposition = await errorPageExtention.didReceive(URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallangeSender()), for: navigation) + + // THEN + if case .credential(let credential) = disposition { + XCTAssertNotNil(credential) + } else { + XCTFail("No credentials found") + } + + // WHEN + disposition = await errorPageExtention.didReceive(URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallangeSender()), for: navigation) + + // THEN + XCTAssertNil(disposition) + } + + @MainActor + func testWhenDidReceiveChallange_IfChallangeNotForCertificateValidation_AndUserRequestBypass_AndNavigationURLIsTheSameAsWevViewURL_ThenReturnsNoCredentials() async { + // GIVEN + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodClientCertificate) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.example.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + errorPageExtention.visitSite() + + // WHEN + let disposition = await errorPageExtention.didReceive(URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallangeSender()), for: navigation) + + // THEN + XCTAssertNil(disposition) + } + + @MainActor + func testWhenDidReceiveChallange_IfChallangeForCertificateValidation_AndUserDoesNotRequestBypass_AndNavigationURLIsTheSameAsWevViewURL_ThenReturnsNoCredentials() async { + // GIVEN + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.example.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + errorPageExtention.leaveSite() + + // WHEN + let disposition = await errorPageExtention.didReceive(URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallangeSender()), for: navigation) + + // THEN + XCTAssertNil(disposition) + } + + @MainActor + func testWhenDidReceiveChallange_IfChallangeNotForCertificateValidation_AndUserDoesNotRequestBypass_AndNavigationURLIsNotTheSameAsWevViewURL_ThenReturnsNoCredentials() async { + // GIVEN + let protectionSpace = URLProtectionSpace(host: "", port: 4, protocol: nil, realm: nil, authenticationMethod: NSURLAuthenticationMethodServerTrust) + let action = NavigationAction(request: URLRequest(url: URL(string: "com.different.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) + let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) + let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) + errorPageExtention.webView = mockWebView + errorPageExtention.visitSite() + + // WHEN + let disposition = await errorPageExtention.didReceive(URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, previousFailureCount: 0, failureResponse: nil, error: nil, sender: ChallangeSender()), for: navigation) + + // THEN + XCTAssertNil(disposition) + } + +} + +class MockWKWebView: NSObject, ErrorPageTabExtensionNavigationDelegate { + var canGoBack: Bool = true + var url: URL? + var capturedHTML: String = "" + var goBackCalled = false + var reloadCalled = false + var closedCalled = false + + init(url: URL) { + self.url = url + } + + func loadAlternateHTML(_ html: String, baseURL: URL, forUnreachableURL failingURL: URL) { + capturedHTML = html + } + + func setDocumentHtml(_ html: String) { + capturedHTML = html + } + + func goBack() -> WKNavigation? { + goBackCalled = true + return nil + } + + func reloadPage() -> WKNavigation? { + reloadCalled = true + return nil + } + + func close() { + closedCalled = true + } +} + +class MockSSLErrorPageScriptProvider: SSLErrorPageScriptProvider { + var sslErrorPageUserScript: SSLErrorPageUserScript? + + init(script: SSLErrorPageUserScript?) { + self.sslErrorPageUserScript = script + } +} + +class MockCredentialCreator: URLCredentialCreating { + func urlCredentialFrom(trust: SecTrust?) -> URLCredential? { + return URLCredential(user: "", password: "", persistence: .forSession) + } +} + +class ChallangeSender: URLAuthenticationChallengeSender { + func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {} + func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {} + func cancel(_ challenge: URLAuthenticationChallenge) {} + func isEqual(_ object: Any?) -> Bool { + return false + } + var hash: Int = 0 + var superclass: AnyClass? + func `self`() -> Self { + self + } + func perform(_ aSelector: Selector!) -> Unmanaged! { + return nil + } + func perform(_ aSelector: Selector!, with object: Any!) -> Unmanaged! { + return nil + } + func perform(_ aSelector: Selector!, with object1: Any!, with object2: Any!) -> Unmanaged! { + return nil + } + func isProxy() -> Bool { + return false + } + func isKind(of aClass: AnyClass) -> Bool { + return false + } + func isMember(of aClass: AnyClass) -> Bool { + return false + } + func conforms(to aProtocol: Protocol) -> Bool { + return false + } + func responds(to aSelector: Selector!) -> Bool { + return false + } + var description: String = "" +} + +class MockFeatureFlagger: FeatureFlagger { + func isFeatureOn(forProvider: F) -> Bool where F: BrowserServicesKit.FeatureFlagSourceProviding { + return true + } +} diff --git a/UnitTests/UserScripts/SSLErrorPageUserScriptTests.swift b/UnitTests/UserScripts/SSLErrorPageUserScriptTests.swift new file mode 100644 index 0000000000..f94b23f889 --- /dev/null +++ b/UnitTests/UserScripts/SSLErrorPageUserScriptTests.swift @@ -0,0 +1,122 @@ +// +// SSLErrorPageUserScriptTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import UserScript + +@testable import DuckDuckGo_Privacy_Browser + +final class SSLErrorPageUserScriptTests: XCTestCase { + + var delegate: CapturingSSLErrorPageUserScriptDelegate! + var userScript: SSLErrorPageUserScript! + + override func setUpWithError() throws { + delegate = CapturingSSLErrorPageUserScriptDelegate() + userScript = SSLErrorPageUserScript() + userScript.delegate = delegate + } + + override func tearDownWithError() throws { + delegate = nil + userScript = nil + } + + func test_FeatureHasCorrectName() throws { + XCTAssertEqual(userScript.featureName, "sslErrorPage") + } + + func test_BrokerIsCorrectlyAdded() throws { + // WHEN + let broker = UserScriptMessageBroker(context: "some contect") + userScript.with(broker: broker) + + // THEN + XCTAssertEqual(userScript.broker, broker) + } + + @MainActor + func test_WhenHandlerForLeaveSiteCalled_AndIsEnabledFalse_ThenNoHandlerIsReturned() { + // WHEN + let handler = userScript.handler(forMethodNamed: "leaveSite") + + // THEN + XCTAssertNil(handler) + } + + @MainActor + func test_WhenHandlerForVisitSiteCalled_AndIsEnabledFalse_ThenNoHandlerIsReturned() { + // WHEN + let handler = userScript.handler(forMethodNamed: "visitSite") + + // THEN + XCTAssertNil(handler) + } + + @MainActor + func test_WhenHandlerForLeaveSiteCalled_AndIsEnabledTrue_ThenLeaveSiteCalled() async { + // GIVEN + var encodable: Encodable? + userScript.isEnabled = true + + // WHEN + let handler = userScript.handler(forMethodNamed: "leaveSite") + if let handler { + encodable = try? await handler(Data(), WKScriptMessage()) + } + + // THEN + XCTAssertNotNil(handler) + XCTAssertNil(encodable) + XCTAssertTrue(delegate.leaveSiteCalled) + XCTAssertFalse(delegate.visitSiteCalled) + } + + @MainActor + func test_WhenHandlerForVisitSiteCalled_AndIsEnabledTrue_ThenVisitSiteCalled() async { + // GIVEN + var encodable: Encodable? + userScript.isEnabled = true + + // WHEN + let handler = userScript.handler(forMethodNamed: "visitSite") + if let handler { + encodable = try? await handler(Data(), WKScriptMessage()) + } + + // THEN + XCTAssertNotNil(handler) + XCTAssertNil(encodable) + XCTAssertTrue(delegate.visitSiteCalled) + XCTAssertFalse(delegate.leaveSiteCalled) + } + +} + +class CapturingSSLErrorPageUserScriptDelegate: SSLErrorPageUserScriptDelegate { + var leaveSiteCalled = false + var visitSiteCalled = false + + func leaveSite() { + leaveSiteCalled = true + } + + func visitSite() { + visitSiteCalled = true + } +} diff --git a/scripts/assets/loc/en.xliff b/scripts/assets/loc/en.xliff new file mode 100644 index 0000000000..36c00babfc --- /dev/null +++ b/scripts/assets/loc/en.xliff @@ -0,0 +1,5106 @@ + + + +
+ +
+ + + DuckDuckGo + DuckDuckGo + Bundle name + + + Allows you to upload photographs and videos + Allows you to upload photographs and videos + Privacy - Camera Usage Description + + + Copyright © 2024 DuckDuckGo. All rights reserved. + Copyright © 2024 DuckDuckGo. All rights reserved. + Copyright (human-readable) + + + Allows you to share your geolocation + Allows you to share your geolocation + Privacy - Location Usage Description + + + Allows you to share your location + Allows you to share your location + Privacy - Location When In Use Usage Description + + + Allows you to share recordings + Allows you to share recordings + Privacy - Microphone Usage Description + + +
+ +
+ +
+ + + + + + + + %@ does not support storing passwords + %@ does not support storing passwords + Data Import disabled checkbox message about a browser (%@) not supporting storing passwords + + + %lld + %lld + + + + %lld tracking attempts blocked + %lld tracking attempts blocked + The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@ + + + **%lld** tracking attempts blocked + **%lld** tracking attempts blocked + The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@ + + + Actual Size + Actual Size + Main Menu View item + + + Add Folder + Add Folder + Add Folder popover: Create folder button + + + Add any details that you think may help us fix the problem + Add any details that you think may help us fix the problem + Data import failure Report dialog suggestion to provide a comments with extra details helping to identify the data import problem. + + + Address + Address + Title of the section of the Identities manager where the user can add/modify an address (street city etc,) + + + Address: + Address: + Add Bookmark dialog bookmark url field heading + + + Always Show + Always Show + Preference for always showing the bookmarks bar + + + Birthday + Birthday + Title of the section of the Identities manager where the user can add/modify a date of birth + + + Bookmark Added + Bookmark Added + Bookmark Added popover title + + + Bookmark import failed. + Bookmark import failed. + Data import summary message of failed bookmarks import. + + + Bookmark import failed: + Bookmark import failed: + Data import summary format of how many bookmarks (%lld) failed to import. + + + Bookmarks Import Complete: + Bookmarks Import Complete: + Bookmarks Data Import result summary headline + + + Bookmarks: + Bookmarks: + Data import summary format of how many bookmarks (%lld) were successfully imported. + + + Bring All to Front + Bring All to Front + Main Menu Window item + + + Capitalize + Capitalize + Main Menu Edit-Transformations item + + + Clear All History… + Clear All History… + Main Menu History item + + + Contact Info + Contact Info + Title of the section of the Identities manager where the user can add/modify contact info (phone, email address) + + + Copy + Copy + Command + + + Country + Country + Title of the section of the Identities manager where the user can add/modify a country (US,UK, Italy etc...) + + + Data Detectors + Data Detectors + Main Menu Edit-Substitutions item + + + Delete + Delete + Command + + + Developer + Developer + Main Menu + + + Display progress + Display progress + + + + DuckDuckGo Help + DuckDuckGo Help + Main Menu Help item + + + DuckDuckGo browser version + DuckDuckGo browser version + Data import failure Report dialog description of a report field providing current DuckDuckGo Browser version + + + DuckDuckGo needs your permission to read the %1$@ bookmarks file. Select the %2$@ folder to import bookmarks. + DuckDuckGo needs your permission to read the %1$@ bookmarks file. Select the %2$@ folder to import bookmarks. + Data import warning that DuckDuckGo browser requires file reading permissions for another browser name (%1$@), and instruction to select its (same browser name - %2$@) bookmarks folder. + + + Duplicate Bookmarks Skipped: + Duplicate Bookmarks Skipped: + Data import summary format of how many duplicate bookmarks (%lld) were skipped during import. + + + Edit… + Edit… + Command + + + Enter Full Screen + Enter Full Screen + Main Menu View item + + + Error message & code + Error message & code + Title of the section of a dialog (form where the user can report feedback) where the error message and the error code are shown + + + Favorite This Page… + Favorite This Page… + Main Menu History item + + + Help + Help + Main Menu Help + + + Hide + Hide + Main Menu > View > Home Button > None item + Preferences > Home Button > None item + + + History + History + Main Menu + + + Home + Home + Main Menu View item + + + Home Button + Home Button + Main Menu > View > Home Button item + + + If your computer prompts you to enter a password prior to import, DuckDuckGo will not see that password. + +Imported passwords are stored securely using encryption. + If your computer prompts you to enter a password prior to import, DuckDuckGo will not see that password. + +Imported passwords are stored securely using encryption. + Warning that Chromium data import would require entering system passwords. + + + Import + Import + Menu item + + + Import Bookmarks + Import Bookmarks + Title of dialog with instruction for the user to import bookmarks from another browser + + + Import Passwords + Import Passwords + Title of dialog with instruction for the user to import passwords from another browser + + + Import Results: + Import Results: + Data Import result summary headline + + + JavaScript Console + JavaScript Console + Main Menu View-Developer item + + + Let’s try doing it manually. It won’t take long. + Let’s try doing it manually. It won’t take long. + Suggestion to switch to a Manual File Data Import when data import fails. + + + Location: + Location: + Add Folder popover: parent folder picker title + + + Make Lower Case + Make Lower Case + Main Menu Edit-Transformations item + + + Make Upper Case + Make Upper Case + Main Menu Edit-Transformations item + + + Manage Bookmarks + Manage Bookmarks + Main Menu History item + + + Merge All Windows + Merge All Windows + Main Menu Window item + + + Minimize + Minimize + Main Menu Window item + + + Never Show + Never Show + Preference for never showing the bookmarks bar on new tab + + + Only Show on New Tab + Only Show on New Tab + Preference for only showing the bookmarks bar on new tab + + + Password import complete. You can now delete the saved passwords file. + Password import complete. You can now delete the saved passwords file. + message about Passwords Data Import completion + + + Password import failed. + Password import failed. + Data import summary message of failed passwords import. + + + Password import failed: + Password import failed: + Data import summary format of how many passwords (%lld) failed to import. + + + Passwords: + Passwords: + Data import summary format of how many passwords (%lld) were successfully imported. + + + Please submit a report to help us fix the issue. + Please submit a report to help us fix the issue. + Data import failure Report dialog title. + + + Recently Closed + Recently Closed + Main Menu History item + + + Reload Page + Reload Page + Main Menu View item + + + Remove + Remove + Remove bookmark button title + + + Reopen All Windows from Last Session + Reopen All Windows from Last Session + Main Menu History item + + + Select %@ Folder… + Select %@ Folder… + + + + Select Profile: + Select Profile: + Browser Profile picker title for Data Import + + + Select data to import: + Select data to import: + Data Import section title for checkboxes of data type to import: Passwords or Bookmarks. + + + Settings… + Settings… + Menu item + + + Show Autofill Shortcut + Show Autofill Shortcut + Main Menu View item + + + Show Bookmarks Shortcut + Show Bookmarks Shortcut + Main Menu View item + + + Show Downloads Shortcut + Show Downloads Shortcut + Main Menu View item + + + Show Left of the Back Button + Show Left of the Back Button + Main Menu > View > Home Button > left position item + + + Show Next Tab + Show Next Tab + Main Menu Window item + + + Show Page Source + Show Page Source + Main Menu View-Developer item + + + Show Previous Tab + Show Previous Tab + Main Menu Window item + + + Show Resources + Show Resources + Main Menu View-Developer item + + + Show Right of the Reload Button + Show Right of the Reload Button + Main Menu > View > Home Button > right position item + + + Show Substitutions + Show Substitutions + Main Menu Edit-Substitutions item + + + Show left of the back button + Show left of the back button + Preferences > Home Button > left position item + + + Show right of the reload button + Show right of the reload button + Preferences > Home Button > right position item + + + Smart Copy/Paste + Smart Copy/Paste + Main Menu Edit-Substitutions item + + + Smart Dashes + Smart Dashes + Main Menu Edit-Substitutions item + + + Smart Links + Smart Links + Main Menu Edit-Substitutions item + + + Smart Quotes + Smart Quotes + Main Menu Edit-Substitutions item + + + Speech + Speech + Main Menu Edit item + + + Start Speaking + Start Speaking + Main Menu Edit-Speech item + + + Stop + Stop + Main Menu View item + + + Stop Speaking + Stop Speaking + Main Menu Edit-Speech item + + + Text Replacement + Text Replacement + Main Menu Edit-Substitutions item + + + That didn’t work either. Please submit a report to help us fix the issue. + That didn’t work either. Please submit a report to help us fix the issue. + Data import failure Report dialog title containing a message that not only automatic data import has failed failed but manual browser data import didn‘t work either. + + + The following information will be sent to DuckDuckGo. No personally identifiable information will be sent. + The following information will be sent to DuckDuckGo. No personally identifiable information will be sent. + Data import failure Report dialog subtitle about the data being collected with the report. + + + The version of the browser you are trying to import from + The version of the browser you are trying to import from + Data import failure Report dialog description of a report field providing version of a browser user is trying to import data from + + + Title: + Title: + Add Bookmark dialog bookmark title field heading + + + Transformations + Transformations + Main Menu Edit item + + + Try importing bookmarks manually instead. + Try importing bookmarks manually instead. + Data import error subtitle: suggestion to import Bookmarks manually by selecting a CSV or HTML file. + + + Try importing passwords manually instead. + Try importing passwords manually instead. + Data import error subtitle: suggestion to import Passwords manually by selecting a CSV or HTML file. + + + View + View + Main Menu View + + + We couldn‘t find any bookmarks. + We couldn‘t find any bookmarks. + Data import error message: Bookmarks weren‘t found. + + + We couldn‘t find any passwords. + We couldn‘t find any passwords. + Data import error message: Passwords weren‘t found. + + + We were unable to import bookmarks directly from %@. + We were unable to import bookmarks directly from %@. + Message when data import fails from a browser. %@ - a browser name + + + We were unable to import passwords directly from %@. + We were unable to import passwords directly from %@. + Message when data import fails from a browser. %@ - a browser name + + + Window + Window + Main Menu + + + You'll be asked to enter your Primary Password for %@. + +Imported passwords are encrypted and only stored on this computer. + You'll be asked to enter your Primary Password for %@. + +Imported passwords are encrypted and only stored on this computer. + Warning that Firefox-based browser name (%@) data import would require entering a Primary Password for the browser. + + + Zoom In + Zoom In + Main Menu View item + + + Zoom Out + Zoom Out + Main Menu View item + + + Continue + Continue + Continue button + + + DuckDuckGo + DuckDuckGo + Application name to be displayed in the About dialog + + + DuckDuckGo for Mac App Store + DuckDuckGo for Mac App Store + Application name to be displayed in the About dialog in App Store app + + + This action cannot be undone. + This action cannot be undone. + Text used in alerts to warn user that a given action cannot be undone + + + Name: + Name: + New bookmark folder dialog folder name field heading + + + Add Favorite + Add Favorite + Button for adding a favorite bookmark + + + Name: + Name: + Add Folder popover: folder name text field title + + + Add Link to Bookmarks + Add Link to Bookmarks + Context menu item + + + Add to Favorites + Add to Favorites + Button for adding bookmarks to favorites + + + Search or enter address + Search or enter address + Empty Address Bar placeholder text displayed on the new tab page. + + + Search DuckDuckGo + Search DuckDuckGo + Suffix of searched terms in address bar. Example: best watching machine . Search DuckDuckGo + + + Visit + Visit + Address bar suffix of possibly visited website. Example: spreadprivacy.com . Visit spreadprivacy.com + + + After installing, return to DuckDuckGo to complete the setup. + After installing, return to DuckDuckGo to complete the setup. + Setup of the integration with Bitwarden app + + + You have exceeded the bookmarks sync limit. Try deleting some bookmarks. Until this is resolved your bookmarks will not be backed up. + You have exceeded the bookmarks sync limit. Try deleting some bookmarks. Until this is resolved your bookmarks will not be backed up. + Description for alert shown when sync bookmarks paused for too many items + + + Bookmarks Sync is Paused + Bookmarks Sync is Paused + Title for alert shown when sync bookmarks paused for too many items + + + You have exceeded the passwords sync limit. Try deleting some passwords. Until this is resolved your passwords will not be backed up. + You have exceeded the passwords sync limit. Try deleting some passwords. Until this is resolved your passwords will not be backed up. + Description for alert shown when sync credentials paused for too many items + + + Passwords Sync is Paused + Passwords Sync is Paused + Title for alert shown when sync credentials paused for too many items + + + Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue. + Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue. + Data syncing unavailable warning message + + + Sync & Backup is Paused + Sync & Backup is Paused + Title of the warning message + + + Sorry, but Sync & Backup is currently unavailable. Please try again later. + Sorry, but Sync & Backup is currently unavailable. Please try again later. + Data syncing unavailable warning message + + + A message from %@ + A message from %@ + Title formatted with presenting domain + + + Allow Integration with DuckDuckGo + Allow Integration with DuckDuckGo + Setup of the integration with Bitwarden app + + + Sign In + Sign In + Authentication Alert Sign In Button + + + Sign in to %@. Your login information will be sent securely. + Sign in to %@. Your login information will be sent securely. + Authentication Alert - populated with a domain name + + + Log in to %@. Your password will be sent insecurely because the connection is unencrypted. + Log in to %@. Your password will be sent insecurely because the connection is unencrypted. + Authentication Alert - populated with a domain name + + + Password + Password + Authentication Password field placeholder + + + Authentication Required + Authentication Required + Authentication Alert Title + + + Username + Username + Authentication User name field placeholder + + + Automatically handle cookie pop-ups + Automatically handle cookie pop-ups + Autoconsent settings checkbox title + + + DuckDuckGo will try to select the most private settings available and hide these pop-ups for you. + DuckDuckGo will try to select the most private settings available and hide these pop-ups for you. + Autoconsent feature explanation in settings + + + When we detect cookie pop-ups on sites you visit, we can try to select the most private settings available and hide pop-ups like this. + When we detect cookie pop-ups on sites you visit, we can try to select the most private settings available and hide pop-ups like this. + Body for modal asking the user to auto manage cookies + + + Handle Pop-ups For Me + Handle Pop-ups For Me + Confirm button for modal asking the user to auto manage cookies + + + Want DuckDuckGo to handle cookie pop-ups? + Want DuckDuckGo to handle cookie pop-ups? + Title for modal asking the user to auto manage cookies + + + Want me to handle these for you? I can try to minimize cookies, maximize privacy, and hide pop-ups like these. + Want me to handle these for you? I can try to minimize cookies, maximize privacy, and hide pop-ups like these. + Body for modal asking the user to auto manage cookies + + + Manage Cookie Pop-ups + Manage Cookie Pop-ups + Confirm button for modal asking the user to auto manage cookies + + + No Thanks + No Thanks + Deny button for modal asking the user to auto manage cookies + + + Looks like this site has a cookie pop-up 👇 + Looks like this site has a cookie pop-up 👇 + Title for modal asking the user to auto manage cookies + + + Cookie Pop-ups + Cookie Pop-ups + Autoconsent settings section title + + + Addresses + Addresses + Autofill autosaved data type + + + Save and Autofill + Save and Autofill + Autofill settings section title + + + Receive prompts to save new information and autofill online forms. + Receive prompts to save new information and autofill online forms. + Description of Autofill autosaving feature - used in settings + + + Auto-lock + Auto-lock + Autofill settings section title + + + Also lock password form fill + Also lock password form fill + Lock form filling when auto-lock is active text + + + Copy password + Copy password + Tooltip for the Autofill panel's Copy Password button + + + Copy username + Copy username + Tooltip for the Autofill panel's Copy Username button + + + Excluded Sites + Excluded Sites + Autofill settings section title + + + Websites you selected to never ask to save your password. + Websites you selected to never ask to save your password. + Subtitle providing additional information about the excluded sites section + + + Reset + Reset + Button title allowing users to reset their list of excluded sites + + + If you reset excluded sites, you will be prompted to save your password next time you sign in to any of these sites. + If you reset excluded sites, you will be prompted to save your password next time you sign in to any of these sites. + Alert title + + + Reset Excluded Sites? + Reset Excluded Sites? + Alert title + + + Hide password + Hide password + Tooltip for the Autofill panel's Hide Password button + + + Lock autofill after computer is idle for + Lock autofill after computer is idle for + Autofill auto-lock setting + + + Card + Card + Used as placeholder when user iserts a credit card of unknown type (e.g. not Visa, Mastercard) + + + Locked + Locked + Locked status for password manager + + + Unlocked + Unlocked + Unlocked status for password manager + + + Never lock autofill + Never lock autofill + Autofill auto-lock setting + + + If not locked, anyone with access to your device will be able to use and modify your autofill data. For security purposes, credit card form fill always requires authentication. + If not locked, anyone with access to your device will be able to use and modify your autofill data. For security purposes, credit card form fill always requires authentication. + Autofill disabled auto-lock warning + + + Password Manager + Password Manager + Autofill settings section title + + + Bitwarden + Bitwarden + Autofill password manager row title + + + Setup requires installing the Bitwarden app. + Setup requires installing the Bitwarden app. + Autofill password manager Bitwarden disclaimer + + + DuckDuckGo built-in password manager + DuckDuckGo built-in password manager + Autofill password manager row title + + + Payment methods + Payment methods + Autofill autosaved data type + + + View + View + Button to view the recently autosaved password + + + Password saved for %@ + Password saved for %@ + Text confirming a password has been saved for the %@ domain + + + Change in + Change in + Suffix of the label - change in settings - + + + Open %@ + Open %@ + Open password manager button + + + Connected to user %@ + Connected to user %@ + Label describing what user is connected to the password manager + + + You're using %@ to manage passwords + You're using %@ to manage passwords + Explanation of what password manager is being used + + + Settings + Settings + Open Settings Button + + + Show password + Show password + Tooltip for the Autofill panel's Show Password button + + + Usernames and passwords + Usernames and passwords + Autofill autosaved data type + + + View Autofill Content… + View Autofill Content… + View Autofill Content Button name in the autofill settings + + + Bitwarden app found! + Bitwarden app found! + Setup of the integration with Bitwarden app + + + DuckDuckGo needs permission to access Bitwarden. You can grant DuckDuckGo Full Disk Access in System Settings, or switch back to the built-in password manager. + DuckDuckGo needs permission to access Bitwarden. You can grant DuckDuckGo Full Disk Access in System Settings, or switch back to the built-in password manager. + Requests user Full Disk access in order to access password manager Birwarden + + + All communication between Bitwarden and DuckDuckGo is encrypted and the data never leaves your device. + All communication between Bitwarden and DuckDuckGo is encrypted and the data never leaves your device. + Warns users that all communication between the DuckDuckGo browser and the password manager Bitwarden is encrypted and doesn't leave the user device + + + We’ll walk you through connecting to Bitwarden, so you can use it in DuckDuckGo. + We’ll walk you through connecting to Bitwarden, so you can use it in DuckDuckGo. + Description for when the user wants to connect the browser to the password manager Bitwarned. + + + Bitwarden will have access to your browsing history. + Bitwarden will have access to your browsing history. + Warn users that the password Manager Bitwarden will have access to their browsing history + + + Privacy + Privacy + + + + Connect to Bitwarden + Connect to Bitwarden + Title for the Bitwarden onboarding flow + + + Connecting to Bitwarden + Connecting to Bitwarden + It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action, in this case we are in the progress of connecting the browser to the Bitwarden password maanger. + + + Unable to find or connect to Bitwarden + Unable to find or connect to Bitwarden + This message appears when the application is unable to find or connect to Bitwarden, indicating a connection issue. + + + Handshake not approved in Bitwarden app + Handshake not approved in Bitwarden app + It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action. This message indicates that the handshake process was not approved in the Bitwarden app. + + + Install Bitwarden + Install Bitwarden + Button to install Bitwarden app + + + To begin setup, first install Bitwarden from the App Store. + To begin setup, first install Bitwarden from the App Store. + Setup of the integration with Bitwarden app + + + Bitwarden integration complete! + Bitwarden integration complete! + Setup of the integration with Bitwarden app + + + You are now using Bitwarden as your password manager. + You are now using Bitwarden as your password manager. + Setup of the integration with Bitwarden app + + + Integration with DuckDuckGo is not approved in Bitwarden app + Integration with DuckDuckGo is not approved in Bitwarden app + While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates that the integration with DuckDuckGo has not been approved in the Bitwarden app. + + + Bitwarden is ready to connect to DuckDuckGo! + Bitwarden is ready to connect to DuckDuckGo! + Setup of the integration with Bitwarden app + + + Missing handshake + Missing handshake + While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates a missing handshake (a way for two devices or systems to say hello to each other and agree to communicate or exchange information). + + + Bitwarden app is not installed + Bitwarden app is not installed + + + + Please update Bitwarden to the latest version + Please update Bitwarden to the latest version + Message that warns user they need to update their password manager Bitwarden app vesion + + + Complete Setup… + Complete Setup… + action option that prompts the user to complete the setup process in Bitwarden preferences + + + Open Bitwarden + Open Bitwarden + Button to open Bitwarden app + + + Bitwarden app not running + Bitwarden app not running + Warns user that the password manager Bitwarden app is not running + + + Unable to find or connect to Bitwarden + Unable to find or connect to Bitwarden + Dialog telling the user Bitwarden (a password manager) is not available + + + Unlock Bitwarden + Unlock Bitwarden + Asks the user to unlock the password manager Bitwarden + + + Waiting for the handshake approval in Bitwarden app + Waiting for the handshake approval in Bitwarden app + While the user tries to connect the DuckDuckGo Browser to password manager Bitwarden This message indicates the system is waiting for the handshake (a way for two devices or systems to say hello to each other and agree to communicate or exchange information). + + + Waiting for permission to use Bitwarden in DuckDuckGo… + Waiting for permission to use Bitwarden in DuckDuckGo… + Setup of the integration with Bitwarden app + + + Waiting for the status response from Bitwarden + Waiting for the status response from Bitwarden + It appears in a dialog when the users are connecting to Bitwardern and shows the status of the action, in this case that the application is currently waiting for a response from the Bitwarden service. + + + Add + Add + Button to confim a bookmark creation + + + Bookmark Page + Bookmark Page + Context menu item + + + Bookmark This Page + Bookmark This Page + Menu item for bookmarking current page + + + Update Bookmark + Update Bookmark + Option for updating a bookmark + + + Bookmarks + Bookmarks + Button for bookmarks + + + New Bookmark + New Bookmark + Bookmark creation dialog title + + + Copy + Copy + Copy menu item for the bookmarks bar context menu + + + Delete + Delete + Delete menu item for the bookmarks bar context menu + + + Move to End + Move to End + Move to End menu item for the bookmarks bar context menu + + + Empty + Empty + Empty state for a bookmarks bar folder + + + Show Bookmarks Bar + Show Bookmarks Bar + Preference item for showing the bookmarks bar + + + Show + Show + Accept button label on bookmarks bar prompt + + + Hide + Hide + Dismiss button label on bookmarks bar prompt + + + Show the Bookmarks Bar for quick access to your new bookmarks. + Show the Bookmarks Bar for quick access to your new bookmarks. + Message show for bookmarks bar prompt + + + Show Bookmarks Bar? + Show Bookmarks Bar? + Title for bookmarks bar prompt + + + Bookmarks Bar + Bookmarks Bar + Menu item for showing the bookmarks bar + + + Always show + Always show + Preference for always showing the bookmarks bar + + + Only show on New Tab + Only show on New Tab + Preference for only showing the bookmarks bar on new tab + + + Add Bookmark + Add Bookmark + CTA title for adding a Bookmark + + + Add Folder + Add Folder + CTA title for adding a Folder + + + Location + Location + Location field label for Bookmark folder + + + Name + Name + Name field label for Bookmark or Folder + + + URL + URL + URL field label for Bookmar + + + Add Folder + Add Folder + Bookmark folder creation dialog title + + + Edit Folder + Edit Folder + Bookmark folder edit dialog title + + + Add bookmark + Add bookmark + Bookmark creation dialog title + + + Bookmark Added + Bookmark Added + Bookmark added popover title + + + Edit bookmark + Edit bookmark + Bookmark edit dialog title + + + If your bookmarks are saved in another browser, you can import them into DuckDuckGo. + If your bookmarks are saved in another browser, you can import them into DuckDuckGo. + Text displayed in Bookmark Manager when there is no bookmarks yet + + + No bookmarks yet + No bookmarks yet + Title displayed in Bookmark Manager when there is no bookmarks yet + + + Import + Import + Button text to open bookmark import dialog + + + Imported from + Imported from + Name of the folder the imported bookmarks are saved into + + + Mobile bookmarks + Mobile bookmarks + Name of the "Mobile bookmarks" folder imported from other browser + + + Other bookmarks + Other bookmarks + Name of the "Other bookmarks" folder imported from other browser + + + Manage + Manage + Button for opening the bookmarks management interface + + + Manage Bookmarks + Manage Bookmarks + Menu item for opening the bookmarks management interface + + + Open in New Tabs + Open in New Tabs + Open all bookmarks in folder in new tabs + + + Open Bookmarks Panel + Open Bookmarks Panel + Menu item for opening the bookmarks panel + + + Browse without saving local history + Browse without saving local history + Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites. + + + Sign in to a site with a different account + Sign in to a site with a different account + Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites. + + + Troubleshoot websites + Troubleshoot websites + Descriptions of features Fire page. Provides information about browsing functionalities such as browsing without saving local history, signing in to a site with a different account, and troubleshooting websites. + + + Fire windows are isolated from other browser data, and their data is burned when you close them. They have the same tracking protection as other windows. + Fire windows are isolated from other browser data, and their data is burned when you close them. They have the same tracking protection as other windows. + This describes the functionality of one of out browser feature Fire Window, highlighting their isolation from other browser data and the automatic deletion of their data upon closure. Additionally, it emphasizes that fire windows offer the same level of tracking protection as other browsing windows. + + + New Fire Tab + New Fire Tab + Tab title for Fire Tab + + + Fire Window + Fire Window + Header shown on the hompage of the Fire Window + + + Cancel + Cancel + Cancel button + + + Cannot Open File + Cannot Open File + Header of the alert dialog informing user it is not possible to open the file + + + The App Store version of DuckDuckGo can only access local files if you drag-and-drop them into a browser window. + + To navigate local files using the address bar, please download DuckDuckGo directly from https://duckduckgo.com/mac. + The App Store version of DuckDuckGo can only access local files if you drag-and-drop them into a browser window. + + To navigate local files using the address bar, please download DuckDuckGo directly from https://duckduckgo.com/mac. + Informative of the alert dialog informing user it is not possible to open the file + + + Check Allow integration with DuckDuckGo. + Check Allow integration with DuckDuckGo. + Setup of the integration with Bitwarden app + + + Check for Update + Check for Update + Button users can use to check for a new update + + + Clear + Clear + Clear button + + + Close Other Tabs + Close Other Tabs + Menu item + + + Close Tab + Close Tab + Menu item + + + Close and Return to Previous Tab + Close and Return to Previous Tab + Close Child Tab on Back Button press and return Back to the Parent Tab without title + + + Close and Return to “%@” + Close and Return to “%@” + Close Child Tab on Back Button press and return Back to the Parent Tab titled “%@” + + + Close Tabs to the Right + Close Tabs to the Right + Menu item + + + Copy + Copy + Copy button + + + Copy + Copy + Copy selection menu item + + + Copy Email Address + Copy Email Address + Context menu item + + + Copy Email Addresses + Copy Email Addresses + Context menu item + + + Copy Image Address + Copy Image Address + Context menu item + + + Click “Send to DuckDuckGo“ to submit report to DuckDuckGo. Crash reports help DuckDuckGo diagnose issues and improve our products. No personal information is sent with this report. + Click “Send to DuckDuckGo“ to submit report to DuckDuckGo. Crash reports help DuckDuckGo diagnose issues and improve our products. No personal information is sent with this report. + Description of the dialog where the user can send a crash report + + + Don’t Send + Don’t Send + Button the user can press to not send the crash report + + + Send to DuckDuckGo + Send to DuckDuckGo + Button the user can press to send the crash report to DuckDuckGo + + + Problem Details + Problem Details + Title of the text field where the problems that caused the crashed are detailed + + + DuckDuckGo Privacy Browser quit unexpectedly. + DuckDuckGo Privacy Browser quit unexpectedly. + Title of the dialog where the user can send a crash report + + + Always allow + Always allow + Privacy Dashboard: Website can always access input media device + + + Always allow on + Always allow on + Permission Popover 'Always allow on' (for domainName) checkbox + + + Ask every time + Ask every time + Privacy Dashboard: Website should always Ask for permission for input media device access + + + Always deny + Always deny + Privacy Dashboard: Website can never access input media device + + + Notify + Notify + Make PopUp Windows always asked from user for current domain + + + Restart your Mac and try again + Restart your Mac and try again + Info to restart macOS after database init failure + + + There was an error initializing the database + There was an error initializing the database + Alert title when we fail to init database + + + Set Default… + Set Default… + represents a prompt message asking the user to make DuckDuckGo their default browser. + + + Make DuckDuckGo your default browser + Make DuckDuckGo your default browser + represents a prompt message asking the user to make DuckDuckGo their default browser. + + + Delete Bookmark + Delete Bookmark + Delete Bookmark button + + + Details + Details + details button + + + Disable + Disable + Email protection Disable button text + + + This will only disable Autofill for Duck Addresses in this browser. + + You can still manually enter Duck Addresses and continue to receive forwarded email. + This will only disable Autofill for Duck Addresses in this browser. + + You can still manually enter Duck Addresses and continue to receive forwarded email. + Message for alert shown when user disables email protection + + + Disable Email Protection Autofill? + Disable Email Protection Autofill? + Title for alert shown when user disables email protection + + + %@ is now Fireproof + %@ is now Fireproof + Domain fireproof status + + + Done + Done + Done button + + + Don’t Quit + Don’t Quit + Don’t Quit button + + + Don't Save + Don't Save + Don't Save button + + + Don't Update + Don't Update + Don't Update button + + + Finishing download… + Finishing download… + Download being finished information text + + + Download Linked File As… + Download Linked File As… + Context menu item + + + Starting download… + Starting download… + Download being initiated information text + + + , and other files + , and other files + Alert text format element for “, and other files” + + + Are you sure you want to quit? DuckDuckGo Privacy Browser is currently downloading “%1$@”%2$@. If you quit now DuckDuckGo Privacy Browser won’t finish downloading this file. + Are you sure you want to quit? DuckDuckGo Privacy Browser is currently downloading “%1$@”%2$@. If you quit now DuckDuckGo Privacy Browser won’t finish downloading this file. + Alert text format when trying to quit application while file “filename”[, and others] are being downloaded + + + A download is in progress. + A download is in progress. + Alert title when trying to quit application while files are being downloaded + + + Always ask where to save files + Always ask where to save files + Downloads preferences checkbox + + + %1$@ of %2$@ + %1$@ of %2$@ + Number of bytes out of total bytes downloaded (1Mb of 2Mb) + + + Change… + Change… + Change downloads directory button + + + Clear All + Clear All + Contextual menu item in downloads manager to clear all downloaded items from the list + + + Copy Download Link + Copy Download Link + Contextual menu item in downloads manager to copy the downloaded link + + + Downloads + Downloads + Title of the dialog that manages the Downloads in the browser + + + Canceled + Canceled + Short error description when downloaded file download was canceled + + + Could not move file to Downloads + Could not move file to Downloads + Short error description when could not move downloaded file to the Downloads folder + + + Error + Error + Short error description when Download failed + + + Removed + Removed + Short error description when downloaded file removed from Downloads folder + + + Location + Location + Downloads directory location + + + No recent downloads + No recent downloads + Label in the downloads manager that shows that there are no recently downloaded items + + + Open Downloads Folder + Open Downloads Folder + Button in the downloads manager that allows the user to open the downloads folder + + + Open Originating Website + Open Originating Website + Contextual menu item in downloads manager to open the downloaded file originating website + + + Open + Open + Contextual menu item in downloads manager to open the downloaded file + + + Automatically open the Downloads panel when downloads complete + Automatically open the Downloads panel when downloads complete + Checkbox to open a Download Manager popover when downloads are completed + + + Remove from List + Remove from List + Contextual menu item in downloads manager to remove the given downloaded from the list of downloaded files + + + Stop + Stop + Contextual menu item in downloads manager to restart the download + + + Show in Finder + Show in Finder + Contextual menu item in downloads manager to show the downloaded file in Finder + + + %@/s + %@/s + Download speed format (1Mb/sec) + + + Stop + Stop + Contextual menu item in downloads manager to stop the download + + + Cancel Download + Cancel Download + Mouse-over tooltip for Cancel Download button + + + Download Again + Download Again + Mouse-over tooltip for Download [deleted file] Again button + + + Restart Download + Restart Download + Mouse-over tooltip for Restart Download button + + + Show in Finder + Show in Finder + Mouse-over tooltip for Show in Finder button + + + Always open YouTube videos in Duck Player + Always open YouTube videos in Duck Player + Private YouTube Player option + + + Duck Player provides a clean viewing experience without personalized ads and prevents viewing activity from influencing your YouTube recommendations. + Duck Player provides a clean viewing experience without personalized ads and prevents viewing activity from influencing your YouTube recommendations. + Private YouTube Player explanation in settings + + + Never use Duck Player + Never use Duck Player + Private YouTube Player option + + + Show option to use Duck Player over YouTube previews on hover + Show option to use Duck Player over YouTube previews on hover + Private YouTube Player option + + + Duck Player + Duck Player + Private YouTube Player settings title + + + Duplicate Tab + Duplicate Tab + Menu item. Duplicate as a verb + + + Edit + Edit + Edit button + + + Edit Favorite + Edit Favorite + Header of the view that edits a favorite bookmark + + + Edit Folder + Edit Folder + Header of the view that edits a bookmark folder + + + New address copied to your clipboard + New address copied to your clipboard + Notification that the Private email address was copied to clipboard after the user generated a new address + + + Email Protection + Email Protection + Menu item email feature + + + Generate Private Duck Address + Generate Private Duck Address + Create an email alias sub menu item + + + Manage Account + Manage Account + Manage private email account sub menu item + + + Disable Email Protection Autofill + Disable Email Protection Autofill + Disable email sub menu item + + + Enable Email Protection + Enable Email Protection + Sub menu item to enable Email Protection + + + An unknown error has occurred + An unknown error has occurred + Generic error message on a dialog for when the cause is not known. + + + Please check that no file exists at the location you selected. + Please check that no file exists at the location you selected. + Alert message when exporting bookmarks fails + + + Failed to Export Bookmarks… + Failed to Export Bookmarks… + Alert title when exporting login data fails + + + Bookmarks + Bookmarks + The last part of the suggested file for exporting bookmarks + + + Export Bookmarks… + Export Bookmarks… + Export bookmarks menu item + + + Export Passwords… + Export Passwords… + Opens Export Logins Data dialog + + + Please check that no file exists at the location you selected. + Please check that no file exists at the location you selected. + Alert message when exporting login data fails + + + Failed to Export Passwords + Failed to Export Passwords + Alert title when exporting login data fails + + + Passwords + Passwords + The last part of the suggested file name for exporting logins + + + This file contains your passwords in plain text and should be saved in a secure location and deleted when you are done. +Anyone with access to this file will be able to read your passwords. + This file contains your passwords in plain text and should be saved in a secure location and deleted when you are done. +Anyone with access to this file will be able to read your passwords. + Warning text presented when exporting logins. + + + Favorites + Favorites + Title text for the Favorites menu item + + + Please describe the problem in as much detail as possible: + Please describe the problem in as much detail as possible: + Label in the feedback form that users can submit to say that a website is not working properly in DuckDuckGo + + + Reports sent to DuckDuckGo are 100% anonymous and only include your message, the DuckDuckGo app version, and your macOS version. + Reports sent to DuckDuckGo are 100% anonymous and only include your message, the DuckDuckGo app version, and your macOS version. + Disclaimer in breakage form - a form that users can submit to say that a website is not working properly in DuckDuckGo + + + What feature would you like to see? + What feature would you like to see? + Label in the feedback form for feature requests. + + + Please give us your feedback: + Please give us your feedback: + Label in the feedback form + + + Find in page + Find in page + Placeholder text for the text field where the user inputs strings to searcg in the web page + + + %1$d of %2$d + %1$d of %2$d + Find in page status (e.g. 1 of 99) + + + Find in Page… + Find in Page… + Menu item title + + + Close active tabs (%1$d) and clear all browsing history and cookies (sites: %2$d). + Close active tabs (%1$d) and clear all browsing history and cookies (sites: %2$d). + Info in the Fire Button popover + + + Clear all tabs and related site data + Clear all tabs and related site data + Description of the 'All Data' configuration option for the fire button + + + All sites + All sites + Configuration option for fire button + + + Clear current window and related site data + Clear current window and related site data + Description of the 'Current Window' configuration option for the fire button + + + All sites visited in current tab + All sites visited in current tab + Configuration option for fire button + + + All sites visited in current window + All sites visited in current window + Configuration option for fire button + + + All windows will close + All windows will close + Warning label shown in an expanded view of the fire popover + + + Selected sites will be cleared + Selected sites will be cleared + Category of domains in fire button dialog + + + Close and Burn This Window + Close and Burn This Window + Button that allows the user to close and burn the browser burner window + + + Deleting browsing data… + Deleting browsing data… + Text shown in dialog while removing browsing data + + + Details + Details + Button to show more details + + + Close Tabs and Clear Data + Close Tabs and Clear Data + Title of the dialog where the user can close browser tabs and clear data. + + + An isolated window that doesn’t save any data + An isolated window that doesn’t save any data + Explanation of what a fire window is. + + + Open New Fire Window + Open New Fire Window + Title of the part of the dialog where the user can open a fire window. + + + Fireproof sites won't be cleared + Fireproof sites won't be cleared + Category of domains in fire button dialog + + + Current tab will close + Current tab will close + Warning label shown in an expanded view of the fire popover + + + Pinned tab will reload + Pinned tab will reload + Warning label shown in an expanded view of the fire popover + + + Current window will close + Current window will close + Warning label shown in an expanded view of the fire popover + + + Data, browsing history, and cookies can build up in your browser over time. Use the Fire Button to clear it all away. + Data, browsing history, and cookies can build up in your browser over time. Use the Fire Button to clear it all away. + Description in the dialog that explains the Fire feature. + + + Leave No Trace + Leave No Trace + Title of the dialog that explains the Fire feature. + + + Close this tab and clear its browsing history and cookies (sites: %d). + Close this tab and clear its browsing history and cookies (sites: %d). + Info in the Fire Button popover + + + Select a site to clear its data. + Select a site to clear its data. + Info label in the fire button popover + + + Clear data only for selected domains + Clear data only for selected domains + Description of the 'Current Window' configuration option for the fire button + + + Fireproof + Fireproof + Fireproof button + + + Ask to Fireproof websites when signing in + Ask to Fireproof websites when signing in + Fireproof settings checkbox title + + + Fireproofing this site will keep you signed in after using the Fire Button. + Fireproofing this site will keep you signed in after using the Fire Button. + Fireproof confirmation message + + + Would you like to Fireproof %@? + Would you like to Fireproof %@? + Fireproof confirmation title + + + Remove All + Remove All + Label of a button that allows the user to remove all the websites from the fireproofed list + + + When you Fireproof a site, cookies won't be erased and you'll stay signed in, even after using the Fire Button. + When you Fireproof a site, cookies won't be erased and you'll stay signed in, even after using the Fire Button. + Fireproofing mechanism explanation + + + Manage Fireproof Sites… + Manage Fireproof Sites… + Fireproof settings button caption + + + Fireproof Sites + Fireproof Sites + Fireproof sites list title + + + Add + Add + Button to confim a bookmark folder creation + + + Delete Folder + Delete Folder + Option for deleting a folder + + + New Folder + New Folder + Option for creating a new folder + + + Rename Folder + Rename Folder + Option for renaming a folder + + + Discard + Discard + Label of a button that allows the user discard an action/change + + + Remove + Remove + Label of a button that allows the user to remove an item + + + Got It! + Got It! + Got it button + + + Enable Global Privacy Control + Enable Global Privacy Control + GPC settings checkbox title + + + Tells participating websites not to sell or share your data. + Tells participating websites not to sell or share your data. + GPC explanation in settings + + + Global Privacy Control (GPC) + Global Privacy Control (GPC) + GPC settings title + + + Cookies and site data for all sites will also be cleared, unless the site is Fireproof. + Cookies and site data for all sites will also be cleared, unless the site is Fireproof. + Description in the alert with the confirmation to clear all data + + + Clear all history and +close all tabs? + Clear all history and +close all tabs? + Alert with the confirmation to clear all history and data + + + Cookies and other data for sites visited on this day will also be cleared unless the site is Fireproof. History from other days will not be cleared. + Cookies and other data for sites visited on this day will also be cleared unless the site is Fireproof. History from other days will not be cleared. + Description in the alert with the confirmation to clear browsing history + + + Clear History for %@? + Clear History for %@? + Alert with the confirmation to clear all data + + + Cookies and other data for sites visited today will also be cleared unless the site is Fireproof. History from other days will not be cleared. + Cookies and other data for sites visited today will also be cleared unless the site is Fireproof. History from other days will not be cleared. + Description in the alert with the confirmation to clear browsing history + + + Clear history for today +and close all tabs? + Clear history for today +and close all tabs? + Alert with the confirmation to clear all data + + + Clear This History… + Clear This History… + Menu item to clear parts of history and data + + + Older… + Older… + Menu item representing older history + + + Recently Visited + Recently Visited + Section header of the history menu + + + History will be cleared for this site, but related data will remain, because this site is Fireproof + History will be cleared for this site, but related data will remain, because this site is Fireproof + Message for an alert displayed when trying to burn a fireproof website + + + Clear History + Clear History + Button caption for the burn fireproof website alert + + + Keep browsing to see how many trackers were blocked + Keep browsing to see how many trackers were blocked + This string represents the message for an empty state item on the home page, encouraging the user to keep browsing to see how many trackers were blocked + + + Recently visited sites appear here + Recently visited sites appear here + This string represents the title for an empty state item on the home page, indicating that recently visited sites will appear here + + + No trackers blocked + No trackers blocked + This string represents a message on the home page indicating that no trackers were blocked + + + No trackers found + No trackers found + This string represents a message on the home page indicating that no trackers were found + + + PAST 7 DAYS + PAST 7 DAYS + Past 7 days in uppercase. + + + No recent activity + No recent activity + This string represents a message in the protection summary on the home page, indicating that there is no recent activity + + + %@ tracking attempts blocked + %@ tracking attempts blocked + The number of tracking attempts blocked in the last 7 days, shown on a new tab, translate as: Tracking attempts blocked: %@ + + + Bookmarks + Bookmarks + Title text for the Bookmarks import option + + + HTML Bookmarks File (for other browsers) + HTML Bookmarks File (for other browsers) + Title text for the HTML Bookmarks importer + + + Importing bookmarks… + Importing bookmarks… + Operation progress info message about indefinite number of bookmarks being imported + + + Importing bookmarks (%d)… + Importing bookmarks (%d)… + Operation progress info message about %d number of bookmarks being imported + + + Select Safari Folder… + Select Safari Folder… + Text for the Safari data import permission button + + + Select Bookmarks HTML File… + Select Bookmarks HTML File… + Button text for selecting HTML Bookmarks file + + + Import Browser Data + Import Browser Data + Import Browser Data dialog title + + + Import Bookmarks… + Import Bookmarks… + Opens Import Browser Data dialog + + + Import Passwords… + Import Passwords… + Opens Import Browser Data dialog + + + %1$d Open and unlock **%2$s** +%3$d Select **File → Export vault** from the Menu Bar +%4$d Select the File Format: **.csv** +%5$d Enter your Bitwarden master password +%6$d Click %7$@ and save the file someplace you can find it (e.g., Desktop) +%8$d %9$@ + %1$d Open and unlock **%2$s** +%3$d Select **File → Export vault** from the Menu Bar +%4$d Select the File Format: **.csv** +%5$d Enter your Bitwarden master password +%6$d Click %7$@ and save the file someplace you can find it (e.g., Desktop) +%8$d %9$@ + Instructions to import Passwords as CSV from Bitwarden. +%2$s - app name (Bitwarden) +%7$@ - hamburger menu icon +%9$@ - “Select Bitwarden CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Click %4$@ to open the application menu then click **Password Manager** +%5$d Click %6$@ **at the top left** of the Password Manager and select **Settings** +%7$d Find “Export Passwords” and click **Download File** +%8$d Save the passwords file someplace you can find it (e.g., Desktop) +%9$d %10$@ + %1$d Open **%2$s** +%3$d Click %4$@ to open the application menu then click **Password Manager** +%5$d Click %6$@ **at the top left** of the Password Manager and select **Settings** +%7$d Find “Export Passwords” and click **Download File** +%8$d Save the passwords file someplace you can find it (e.g., Desktop) +%9$d %10$@ + Instructions to import Passwords as CSV from Brave browser. +%N$d - step number +%2$s - browser name (Brave) +%4$@, %6$@ - hamburger menu icon +%10$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d In a fresh tab, click %4$@ then **Google Password Manager → Settings** +%5$d Find “Export Passwords” and click **Download File** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d In a fresh tab, click %4$@ then **Google Password Manager → Settings** +%5$d Find “Export Passwords” and click **Download File** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Passwords as CSV from Google Chrome browser. +%N$d - step number +%2$s - browser name (Chrome) +%4$@ - hamburger menu icon +%8$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d In a fresh tab, click %4$@ then **Password Manager → Settings** +%5$d Find “Export Passwords” and click **Download File** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d In a fresh tab, click %4$@ then **Password Manager → Settings** +%5$d Find “Export Passwords” and click **Download File** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Passwords as CSV from Chromium-based browsers. +%N$d - step number +%2$s - browser name +%4$@ - hamburger menu icon +%8$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Type “_coccoc://settings/passwords_” into the Address bar +%4$d Click %5$@ (on the right from _Saved Passwords_) and select **Export passwords** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Type “_coccoc://settings/passwords_” into the Address bar +%4$d Click %5$@ (on the right from _Saved Passwords_) and select **Export passwords** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Passwords as CSV from Cốc Cốc browser. +%N$d - step number +%2$s - browser name (Cốc Cốc) +%5$@ - hamburger menu icon +%8$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Click %4$@ to open the application menu then click **Passwords** +%5$d Click %6$@ then **Export Logins…** +%7$d Save the passwords file someplace you can find it (e.g., Desktop) +%8$d %9$@ + %1$d Open **%2$s** +%3$d Click %4$@ to open the application menu then click **Passwords** +%5$d Click %6$@ then **Export Logins…** +%7$d Save the passwords file someplace you can find it (e.g., Desktop) +%8$d %9$@ + Instructions to import Passwords as CSV from Firefox. +%N$d - step number +%2$s - browser name (Firefox) +%4$@, %6$@ - hamburger menu icon +%9$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + The CSV importer will try to match column headers to their position. +If there is no header, it supports two formats: +%1$d URL, Username, Password +%2$d Title, URL, Username, Password +%3$@ + The CSV importer will try to match column headers to their position. +If there is no header, it supports two formats: +%1$d URL, Username, Password +%2$d Title, URL, Username, Password +%3$@ + Instructions to import a generic CSV passwords file. +%N$d - step number +%3$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Click on the **%2$s** icon in your browser and enter your master password +%3$d Select **Open My Vault** +%4$d From the sidebar select **Advanced Options → Export** +%5$d Enter your LastPass master password +%6$d Select the File Format: **Comma Delimited Text (.csv)** +%7$d %8$@ + %1$d Click on the **%2$s** icon in your browser and enter your master password +%3$d Select **Open My Vault** +%4$d From the sidebar select **Advanced Options → Export** +%5$d Enter your LastPass master password +%6$d Select the File Format: **Comma Delimited Text (.csv)** +%7$d %8$@ + Instructions to import Passwords as CSV from LastPass. +%2$s - app name (LastPass) +%8$@ - “Select LastPass CSV File” button +**bold text**; _italic text_ + + + %1$d Open and unlock **%2$s** +%3$d Select the vault you want to export (you can only export one vault at a time) +%4$d Select **File → Export → All Items** from the Menu Bar +%5$d Enter your 1Password master or account password +%6$d Select the File Format: **iCloud Keychain (.csv)** +%7$d Save the passwords file someplace you can find it (e.g., Desktop) +%8$d %9$@ + %1$d Open and unlock **%2$s** +%3$d Select the vault you want to export (you can only export one vault at a time) +%4$d Select **File → Export → All Items** from the Menu Bar +%5$d Enter your 1Password master or account password +%6$d Select the File Format: **iCloud Keychain (.csv)** +%7$d Save the passwords file someplace you can find it (e.g., Desktop) +%8$d %9$@ + Instructions to import Passwords as CSV from 1Password 7. +%2$s - app name (1Password) +%9$@ - “Select 1Password CSV File” button +**bold text**; _italic text_ + + + %1$d Open and unlock **%2$s** +%3$d Select **File → Export** from the Menu Bar and choose the account you want to export +%4$d Enter your 1Password account password +%5$d Select the File Format: **CSV (Logins and Passwords only)** +%6$d Click Export Data and save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open and unlock **%2$s** +%3$d Select **File → Export** from the Menu Bar and choose the account you want to export +%4$d Enter your 1Password account password +%5$d Select the File Format: **CSV (Logins and Passwords only)** +%6$d Click Export Data and save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Passwords as CSV from 1Password 8. +%2$s - app name (1Password) +%8$@ - “Select 1Password CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **View → Show Password Manager** +%4$d Select **Settings** +%5$d Find “Export Passwords” and click **Download File** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **View → Show Password Manager** +%4$d Select **Settings** +%5$d Find “Export Passwords” and click **Download File** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Passwords as CSV from Opera browser. +%N$d - step number +%2$s - browser name (Opera) +%8$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **View → Show Password Manager** +%4$d Click %5$@ (on the right from _Saved Passwords_) and select **Export passwords** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **View → Show Password Manager** +%4$d Click %5$@ (on the right from _Saved Passwords_) and select **Export passwords** +%6$d Save the passwords file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Passwords as CSV from Opera GX browsers. +%N$d - step number +%2$s - browser name (Opera GX) +%5$@ - menu button icon +%8$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **Safari** +%2$d Select **File → Export → Passwords** +%3$d Save the passwords file someplace you can find it (e.g., Desktop) +%4$d %5$@ + %1$d Open **Safari** +%2$d Select **File → Export → Passwords** +%3$d Save the passwords file someplace you can find it (e.g., Desktop) +%4$d %5$@ + Instructions to import Passwords as CSV from Safari. +%N$d - step number +%5$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Type “_chrome://settings/passwords_” into the Address bar +%4$d Click %5$@ (on the right from _Saved Passwords_) and select **Export passwords** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Type “_chrome://settings/passwords_” into the Address bar +%4$d Click %5$@ (on the right from _Saved Passwords_) and select **Export passwords** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Passwords exported as CSV from Vivaldi browser. +%N$d - step number +%2$s - browser name (Vivaldi) +%5$@ - menu button icon +%8$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Click %4$@ to open the application menu then click **Passwords and cards** +%5$d Click %6$@ then **Export passwords** +%7$d Choose **To a text file (not secure)** and click **Export** +%8$d Save the passwords file someplace you can find it (e.g., Desktop) +%9$d %10$@ + %1$d Open **%2$s** +%3$d Click %4$@ to open the application menu then click **Passwords and cards** +%5$d Click %6$@ then **Export passwords** +%7$d Choose **To a text file (not secure)** and click **Export** +%8$d Save the passwords file someplace you can find it (e.g., Desktop) +%9$d %10$@ + Instructions to import Passwords as CSV from Yandex Browser. +%N$d - step number +%2$s - browser name (Yandex) +%4$@ - hamburger menu icon +%8$@ - “Select Passwords CSV File” button +**bold text**; _italic text_ + + + Cancel + Cancel + Cancel button for data import alerts + + + Import + Import + Import button for data import alerts + + + Done + Done + Button text for finishing the data import + + + Import + Import + Button text for importing data + + + Manual import… + Manual import… + Button text for initiating manual data import using a HTML or CSV file when automatic import has failed + + + DuckDuckGo won't save or share your %1$@ Primary Password, but DuckDuckGo needs it to access and import passwords from %1$@. + DuckDuckGo won't save or share your %1$@ Primary Password, but DuckDuckGo needs it to access and import passwords from %1$@. + Alert body text when the data import needs a password + + + Enter Primary Password for %@ + Enter Primary Password for %@ + Alert title text when the data import needs a password + + + Skip + Skip + Button text to skip an import step + + + Skip bookmarks + Skip bookmarks + Button text to skip bookmarks manual import + + + Skip passwords + Skip passwords + Button text to skip bookmarks manual import + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Bookmark Manager** +%4$d Click %5$@ then **Export Bookmarks** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Bookmark Manager** +%4$d Click %5$@ then **Export Bookmarks** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Bookmarks exported as HTML from Chromium-based browsers. +%N$d - step number +%2$s - browser name +%5$@ - hamburger menu icon +%8$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Manage Bookmarks** +%4$d Click %5$@ then **Export bookmarks to HTML…** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Manage Bookmarks** +%4$d Click %5$@ then **Export bookmarks to HTML…** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Bookmarks exported as HTML from Firefox based browsers. +%N$d - step number +%2$s - browser name (Firefox) +%5$@ - hamburger menu icon +%8$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + %1$d Open your old browser +%2$d Open **Bookmark Manager** +%3$d Export bookmarks to HTML… +%4$d Save the file someplace you can find it (e.g., Desktop) +%5$d %6$@ + %1$d Open your old browser +%2$d Open **Bookmark Manager** +%3$d Export bookmarks to HTML… +%4$d Save the file someplace you can find it (e.g., Desktop) +%5$d %6$@ + Instructions to import a generic HTML Bookmarks file. +%N$d - step number +%6$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Bookmarks** +%4$d Click **Open full Bookmarks view…** in the bottom left +%5$d Click **Import/Export…** in the bottom left and select **Export Bookmarks** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Bookmarks** +%4$d Click **Open full Bookmarks view…** in the bottom left +%5$d Click **Import/Export…** in the bottom left and select **Export Bookmarks** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Bookmarks exported as HTML from Opera browser. +%N$d - step number +%2$s - browser name (Opera) +%8$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Bookmarks** +%4$d Click **Import/Export…** in the bottom left and select **Export Bookmarks** +%5$d Save the file someplace you can find it (e.g., Desktop) +%6$d %7$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Bookmarks → Bookmarks** +%4$d Click **Import/Export…** in the bottom left and select **Export Bookmarks** +%5$d Save the file someplace you can find it (e.g., Desktop) +%6$d %7$@ + Instructions to import Bookmarks exported as HTML from Opera GX browser. +%N$d - step number +%2$s - browser name (Opera GX) +%7$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + %1$d Open **Safari** +%2$d Select **File → Export → Bookmarks** +%3$d Save the passwords file someplace you can find it (e.g., Desktop) +%4$d %5$@ + %1$d Open **Safari** +%2$d Select **File → Export → Bookmarks** +%3$d Save the passwords file someplace you can find it (e.g., Desktop) +%4$d %5$@ + Instructions to import Bookmarks exported as HTML from Safari. +%N$d - step number +%5$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **File → Export Bookmarks…** +%4$d Save the file someplace you can find it (e.g., Desktop) +%5$d %6$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **File → Export Bookmarks…** +%4$d Save the file someplace you can find it (e.g., Desktop) +%5$d %6$@ + Instructions to import Bookmarks exported as HTML from Vivaldi browser. +%N$d - step number +%2$s - browser name (Vivaldi) +%6$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Favorites → Bookmark Manager** +%4$d Click %5$@ then **Export bookmarks to HTML file** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + %1$d Open **%2$s** +%3$d Use the Menu Bar to select **Favorites → Bookmark Manager** +%4$d Click %5$@ then **Export bookmarks to HTML file** +%6$d Save the file someplace you can find it (e.g., Desktop) +%7$d %8$@ + Instructions to import Bookmarks exported as HTML from Yandex Browser. +%N$d - step number +%2$s - browser name (Yandex) +%5$@ - hamburger menu icon +%8$@ - “Select Bookmarks HTML File” button +**bold text**; _italic text_ + + + CSV Passwords File (for other browsers) + CSV Passwords File (for other browsers) + Title text for the CSV importer + + + Passwords + Passwords + Title text for the Passwords import option + + + Select Passwords CSV File… + Select Passwords CSV File… + Button text for selecting a CSV file + + + Select %@ CSV File… + Select %@ CSV File… + Button text for selecting a CSV file exported from (LastPass or Bitwarden or 1Password - %@) + + + You can find your version by selecting **%1$s → About %2$s** from the Menu Bar. + You can find your version by selecting **%1$s → About %2$s** from the Menu Bar. + Instructions how to find an installed 1Password password manager app version. +%1$s, %2$s - app name (1Password) + + + Importing passwords… + Importing passwords… + Operation progress info message about indefinite number of passwords being imported + + + Importing passwords (%d)… + Importing passwords (%d)… + Operation progress info message about %d number of passwords being imported + + + Get Started + Get Started + Get Started button on an invite dialog + + + We didn’t recognize this Invite Code. + We didn’t recognize this Invite Code. + Message to show after user enters an unrecognized invite code + + + Learn More + Learn More + Learn More link + + + Bitwarden not installed… + Bitwarden not installed… + Setup of the integration with Bitwarden app + + + macOS version + macOS version + Data import failure Report dialog description of a report field providing user‘s macOS version + + + Check for Updates… + Check for Updates… + Main Menu DuckDuckGo item + + + Hide DuckDuckGo + Hide DuckDuckGo + Main Menu DuckDuckGo item + + + Hide Others + Hide Others + Main Menu DuckDuckGo item + + + Preferences… + Preferences… + Main Menu DuckDuckGo item + + + Quit DuckDuckGo + Quit DuckDuckGo + Main Menu DuckDuckGo item + + + Services + Services + Main Menu DuckDuckGo item + + + Show All + Show All + Main Menu DuckDuckGo item + + + Edit + Edit + Main Menu Edit + + + Copy + Copy + Main Menu Edit item + + + Cut + Cut + Main Menu Edit item + + + Delete + Delete + Main Menu Edit item + + + Spelling and Grammar + Spelling and Grammar + Main Menu Edit item + + + Find + Find + Main Menu Edit item + + + Find Next + Find Next + Main Menu Edit-Find item + + + Find Previous + Find Previous + Main Menu Edit-Find item + + + Hide Find + Hide Find + Main Menu Edit-Find item + + + Paste + Paste + Main Menu Edit item + + + Paste and Match Style + Paste and Match Style + Main Menu Edit item - Action that allows the user to paste copy into a target document and the target document's style will be retained (instead of the source style) + + + Redo + Redo + Main Menu Edit item + + + Select All + Select All + Main Menu Edit item + + + Check Document Now + Check Document Now + Main Menu Edit-Spellingand item + + + Check Grammar With Spelling + Check Grammar With Spelling + Main Menu Edit-Spellingand item + + + Check Spelling While Typing + Check Spelling While Typing + Main Menu Edit-Spellingand item + + + Correct Spelling Automatically + Correct Spelling Automatically + Main Menu Edit-Spellingand item + + + Show Spelling and Grammar + Show Spelling and Grammar + Main Menu Edit-Spellingand item + + + Substitutions + Substitutions + Main Menu Edit item + + + Undo + Undo + Main Menu Edit item + + + File + File + Main Menu File + + + Close All Windows + Close All Windows + Main Menu File item + + + Close Window + Close Window + Main Menu File item + + + Export + Export + Main Menu File item + + + Bookmarks… + Bookmarks… + Main Menu File-Export item + + + Passwords… + Passwords… + Main Menu File-Export item + + + Import Bookmarks and Passwords… + Import Bookmarks and Passwords… + Main Menu File item + + + New Tab + New Tab + Main Menu File item + + + Open Location… + Open Location… + Main Menu File item- Menu option that allows the user to connect to an address (type an address) on click the address bar of the browser is selected and the user can type. + + + Save As… + Save As… + Main Menu File item + + + Hide Downloads + Hide Downloads + Hide Downloads Popover + + + Close Developer Tools + Close Developer Tools + Hide Web Inspector/Close Developer Tools + + + Show Downloads + Show Downloads + Show Downloads Popover + + + Open Developer Tools + Open Developer Tools + Show Web Inspector/Open Developer Tools + + + Add Folder… + Add Folder… + Menu item to add a folder + + + Edit… + Edit… + Menu item to edit a bookmark or a folder + + + New Tab + New Tab + Context menu item + + + Change Default Page Zoom… + Change Default Page Zoom… + Default page zoom picker title + + + Show Less + Show Less + For collapsing views to show less. + + + Show More + Show More + For expanding views to show more. + + + Mute Tab + Mute Tab + Menu item. Mute tab + + + Window with multiple tabs (%d) + Window with multiple tabs (%d) + String in Recently Closed menu item for recently closed browser window and number of tabs contained in the closed window + + + Back + Back + Context menu item + + + Forward + Forward + Context menu item + + + Never Ask for This Site + Never Ask for This Site + Never ask to save login credentials for this site button + + + New Fire Window + New Fire Window + Menu item title + + + New Window + New Window + Menu item title + + + New Tab Page + New Tab Page + Title of the popover that appears when pressing the bottom right button + + + Favorites + Favorites + Title of the Favorites section in the home page + + + Show Next Steps + Show Next Steps + Title of the menu item in the home page to show/hide continue setup section + + + Show Favorites + Show Favorites + Title of the menu item in the home page to show/hide favorite section + + + Show Recent Activity + Show Recent Activity + Title of the menu item in the home page to show/hide recent activity section + + + Recent Activity + Recent Activity + Title of the RecentActivity section in the home page + + + Import Now + Import Now + Action title on the action menu of the Import card of the Set Up section in the home page + + + Make Default Browser + Make Default Browser + Action title on the action menu of the Default Browser card + + + We automatically block trackers as you browse. It's privacy, simplified. + We automatically block trackers as you browse. It's privacy, simplified. + Summary of the Default Browser card + + + Default to Privacy + Default to Privacy + Title of the Default Browser card of the Set Up section in the home page + + + Try Duck Player + Try Duck Player + Action title on the action menu of the Duck Player card of the Set Up section in the home page + + + Enjoy a clean viewing experience without personalized ads. + Enjoy a clean viewing experience without personalized ads. + Summary of the Duck Player card of the Set Up section in the home page + + + Clean Up YouTube + Clean Up YouTube + Title of the Duck Player card of the Set Up section in the home page + + + Get a Duck Address + Get a Duck Address + Action title on the action menu of the Email Protection card of the Set Up section in the home page + + + Generate custom @duck.com addresses that clean trackers from incoming email. + Generate custom @duck.com addresses that clean trackers from incoming email. + Summary of the Email Protection card of the Set Up section in the home page + + + Protect Your Inbox + Protect Your Inbox + Title of the Email Protection card of the Set Up section in the home page + + + Import bookmarks, favorites, and passwords from your old browser. + Import bookmarks, favorites, and passwords from your old browser. + Summary of the Import card of the Set Up section in the home page + + + Bring Your Stuff + Bring Your Stuff + Title of the Import card of the Set Up section in the home page + + + Dismiss + Dismiss + Action title on the action menu of the set up cards card of the SetUp section in the home page to remove the item + + + Next Steps + Next Steps + Title of the setup section in the home page + + + Share Your Thoughts + Share Your Thoughts + Action title of the Day 0 durvey of the Set Up section in the home page + + + Take our short survey and help us build the best browser. + Take our short survey and help us build the best browser. + Summary of the Day 0 durvey of the Set Up section in the home page + + + Tell Us What Brought You Here + Tell Us What Brought You Here + Title of the Day 0 durvey of the Set Up section in the home page + + + Share Your Thoughts + Share Your Thoughts + Action title of the Day 7 durvey of the Set Up section in the home page + + + Take our short survey and help us build the best browser. + Take our short survey and help us build the best browser. + Summary of the Day 7 durvey of the Set Up section in the home page + + + Help Us Improve + Help Us Improve + Title of the Day 7 durvey of the Set Up section in the home page + + + Next + Next + Next button + + + DuckDuckGo needs permission to access your Downloads folder + DuckDuckGo needs permission to access your Downloads folder + Header of the alert dialog warning the user they need to give the browser permission to access the Downloads folder + + + Grant access in Security & Privacy preferences in System Settings. + Grant access in Security & Privacy preferences in System Settings. + Alert presented to user if the app doesn't have rights to access Downloads folder. This is used for macOS version 12 and below + + + Grant access in Privacy & Security preferences in System Settings. + Grant access in Privacy & Security preferences in System Settings. + Alert presented to user if the app doesn't have rights to access Downloads folder. This is used for macOS version 13 and above + + + Grant access to the location of download. + Grant access to the location of download. + Alert presented to user if the app doesn't have rights to access selected folder + + + DuckDuckGo needs permission to access selected folder + DuckDuckGo needs permission to access selected folder + Header of the alert dialog informing user about failed download + + + Cookies Managed + Cookies Managed + Notification that appears when browser automatically handle cookies + + + Pop-up Hidden + Pop-up Hidden + Notification that appears when browser cosmetically hides a cookie popup + + + Not Now + Not Now + Not Now button + + + OK + OK + OK button + + + Import + Import + Launch the import data UI + + + First, let me help you import your bookmarks 📖 and passwords 🔑 from those less private browsers. + First, let me help you import your bookmarks 📖 and passwords 🔑 from those less private browsers. + Call to action to import data from other browsers + + + Maybe Later + Maybe Later + Skip a step of the onboarding flow + + + Let's Do It! + Let's Do It! + Launch the set default UI + + + Next, try setting DuckDuckGo as your default️ browser, so you can open links with peace of mind, every time. + Next, try setting DuckDuckGo as your default️ browser, so you can open links with peace of mind, every time. + Call to action to set the browser as default + + + You’re all set! + +Want to see how I protect you? Try visiting one of your favorite sites 👆 + +Keep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possibleu{00A0}🔒 + You’re all set! + +Want to see how I protect you? Try visiting one of your favorite sites 👆 + +Keep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possibleu{00A0}🔒 + Call to action to start using the app as a browser + + + Get Started + Get Started + Start the onboarding flow + + + Tired of being tracked online? You've come to the right place 👍 + +I'll help you stay private️ as you search and browse the web. Trackers be gone! + Tired of being tracked online? You've come to the right place 👍 + +I'll help you stay private️ as you search and browse the web. Trackers be gone! + Detailed welcome to the app text + + + Welcome to DuckDuckGo! + Welcome to DuckDuckGo! + General welcome to the app title + + + Open + Open + Open button + + + Open All in New Tabs + Open All in New Tabs + Menu item that opens all the bookmarks in a folder to new tabs + + + Open All in New Window + Open All in New Window + Menu item that opens all the bookmarks in a folder in a new window + + + Open Bitwarden + Open Bitwarden + Button to open Bitwarden app + + + Open Bitwarden and Log in or Unlock your vault. + Open Bitwarden and Log in or Unlock your vault. + Setup of the integration with Bitwarden app + + + The app required to open that link can’t be found + The app required to open that link can’t be found + ’Link’ is link on a website, it couldn't be opened due to the required app not being found + + + Open Image in New Fire Tab + Open Image in New Fire Tab + Context menu item + + + Open Image in New Tab + Open Image in New Tab + Context menu item + + + Open in %@ + Open in %@ + Opening an entity in other application + + + Open in New Tab + Open in New Tab + Menu item that opens the link in a new tab + + + Open in New Window + Open in New Window + Menu item that opens the link in a new window + + + Open Link in New Fire Tab + Open Link in New Fire Tab + Context menu item + + + Open Link in New Tab + Open Link in New Tab + Context menu item + + + Open System Preferences + Open System Preferences + Open System Preferences (to re-enable permission for the App) (up to and including macOS 12 + + + Open System Settings… + Open System Settings… + This string represents a prompt or button label prompting the user to open system settings + + + Fireproof This Site + Fireproof This Site + Context menu item + + + Move Tab to New Window + Move Tab to New Window + Context menu item + + + Remove Fireproofing + Remove Fireproofing + Context menu item + + + This webpage has crashed. + This webpage has crashed. + Error page heading text shown when a Web Page process had crashed + + + Try reloading the page or come back later. + Try reloading the page or come back later. + Error page message text shown when a Web Page process had crashed + + + DuckDuckGo can’t load this page. + DuckDuckGo can’t load this page. + Error page heading text + + + Autofill + Autofill + Used as title for password management user interface + + + All Items + All Items + Used as title for the Autofill All Items option + + + Credit Cards + Credit Cards + Used as title for the Autofill Credit Cards option + + + Identities + Identities + Used as title for the Autofill Identities option + + + Lock + Lock + Lock Logins Vault menu + + + Passwords + Passwords + Used as title for the Autofill Logins option + + + Notes + Notes + Used as title for the Autofill Notes option + + + Save Address? + Save Address? + Title of dialog that allows the user to save an address method + + + Connected to %@ + Connected to %@ + In the password manager dialog, label that specifies the password manager vault we are connected with + + + Keeps you signed in after using the Fire Button + Keeps you signed in after using the Fire Button + In the password manager dialog, description of the section that allows the user to fireproof a website via a checkbox + + + Fireproof? + Fireproof? + In the password manager dialog, title of the section that allows the user to fireproof a website via a checkbox + + + Save Login to Bitwarden? + Save Login to Bitwarden? + Title of the passwored manager section of dialog that allows the user to save credentials + + + Unlock Bitwarden to Save + Unlock Bitwarden to Save + In the password manager dialog, alerts the user that they need to unlock Bitworden before being able to save the credential + + + Save Payment Method? + Save Payment Method? + Title of dialog that allows the user to save a payment method + + + Unlock + Unlock + Unlock Logins Vault menu + + + Delete + Delete + Button of the alert that asks the user to confirm they want to delete an password, login or credential to actually delete + + + Duplicate Password + Duplicate Password + Title of the alert that the password inserted already exists + + + You already have a password saved for this username and website. + You already have a password saved for this username and website. + Text of the alert that explains the password inserted already exists for a given website + + + Are you sure you want to delete this saved credit card? + Are you sure you want to delete this saved credit card? + Text of the alert that asks the user to confirm they want to delete a credit card + + + Are you sure you want to delete this saved autofill info? + Are you sure you want to delete this saved autofill info? + Text of the alert that asks the user to confirm they want to delete an identity + + + Are you sure you want to delete this note? + Are you sure you want to delete this note? + Text of the alert that asks the user to confirm they want to delete a note + + + Are you sure you want to delete this saved password + Are you sure you want to delete this saved password + Text of the alert that asks the user to confirm they want to delete a password + + + Save the changes you made? + Save the changes you made? + Text of the alert that asks the user if the want to save the changes made + + + If your logins are saved in another browser, you can import them into DuckDuckGo. + If your logins are saved in another browser, you can import them into DuckDuckGo. + In the password manager message when there are no items + + + No logins or credit card info yet + No logins or credit card info yet + In the password manager title when there are no items + + + Unlock your Autofill info + Unlock your Autofill info + In the password manager text of button to unlock autofill info + + + Password Manager + Password Manager + Section header + + + Paste from Clipboard + Paste from Clipboard + Paste button + + + Paste & Go + Paste & Go + Paste & Go button + + + Paste & Search + Paste & Search + Paste & Search button + + + Allow “%1$@“ to open %2$@ + Allow “%1$@“ to open %2$@ + Allow to open External Link (%@ 2) to open on current domain (%@ 1) + + + Allow the %1$@ to open “%2$@” links + Allow the %1$@ to open “%2$@” links + Allow the App Name(%@ 1) to open “URL Scheme”(%@ 2) links + + + Open this link in %@? + Open this link in %@? + Popover asking to open link in External App (%@) + + + “%1$@” would like to open this link in %2$@ + “%1$@” would like to open this link in %2$@ + Popover asking for domain %@ to open link in External App (%@) + + + Allow “%1$@“ to use your %2$@? + Allow “%1$@“ to use your %2$@? + Popover asking for domain %@ to use camera/mic/location (%@) + + + Allow “%@“ to open PopUp Window? + Allow “%@“ to open PopUp Window? + Popover asking for domain %@ to open Popup Window + + + Allow “%@“ to open PopUp Windows? + Allow “%@“ to open PopUp Windows? + Popover asking for domain %@ to open Popup Window + + + Camera + Camera + Camera input media device name + + + Camera and Microphone + Camera and Microphone + camera and microphone input media devices name + + + %1$@ access is disabled for %2$@ + %1$@ access is disabled for %2$@ + The app (DuckDuckGo: %@ 2) has no access permission to (%@ 1) media device + + + System location services are disabled + System location services are disabled + Geolocation Services are disabled in System Preferences + + + Open %@ + Open %@ + Open %@ App Name + + + Location + Location + User's Geolocation permission access name + + + Microphone + Microphone + Microphone input media device name + + + Pause %1$@ use on “%2$@” + Pause %1$@ use on “%2$@” + Temporarily pause input media device %@ access for %@2 website + + + Open System Settings + Open System Settings + Open System Settings (to re-enable permission for the App) (macOS 13 and above) + + + Deny + Deny + Permission Popover: Deny Website input media device access + + + Allow + Allow + Button that the user can use to authorise a web site to for, for example access location or camera and microphone etc. + + + Pop-up Blocked + Pop-up Blocked + Text of popver warning the user that the a pop-up as been blocked + + + Learn more about location services + Learn more about location services + Text of link that leads to web page with more informations about location services. + + + %@ + %@ + Open %@ URL Pop-up + + + Blocked Pop-ups + Blocked Pop-ups + Title of a popup that has a list of blocked popups + + + Pop-ups + Pop-ups + Open Pop Up Windows permission access name + + + Reload to ask permission again + Reload to ask permission again + Reload webpage to ask for input media device access permission again + + + Resume %1$@ use on “%2$@” + Resume %1$@ use on “%2$@” + Resume input media device %@ access for %@ website + + + Pin Tab + Pin Tab + Menu item. Pin as a verb + + + Hide Autofill Shortcut + Hide Autofill Shortcut + Menu item for hiding the autofill shortcut + + + Hide Bookmarks Shortcut + Hide Bookmarks Shortcut + Menu item for hiding the bookmarks shortcut + + + Hide Downloads Shortcut + Hide Downloads Shortcut + Menu item for hiding the downloads shortcut + + + Hide VPN Shortcut + Hide VPN Shortcut + Menu item for hiding the NetP shortcut + + + Show Autofill Shortcut + Show Autofill Shortcut + Menu item for showing the autofill shortcut + + + Show Bookmarks Shortcut + Show Bookmarks Shortcut + Menu item for showing the bookmarks shortcut + + + Show Downloads Shortcut + Show Downloads Shortcut + Menu item for showing the downloads shortcut + + + Show VPN Shortcut + Show VPN Shortcut + Menu item for showing the NetP shortcut + + + Reactivate + Reactivate + Activate button + + + Reactivate Duck Address + Reactivate Duck Address + Activate private email address button + + + Add Credit Card + Add Credit Card + Add New Credit Card button + + + Add Identity + Add Identity + Add New Identity button + + + Add Password + Add Password + Add New Login button + + + Add New + Add New + Add New item button + + + Added + Added + Label for login added data + + + Address 1 + Address 1 + Label for address 1 title + + + Address 2 + Address 2 + Label for address 2 title + + + City + City + Label for city title + + + Postal Code + Postal Code + Label for postal code title + + + State/Province + State/Province + Label for state/province title + + + Cancel + Cancel + Cancel button + + + Cardholder Name + Cardholder Name + Label for cardholder name title + + + CVV + CVV + Label for CVV title + + + Expiration Date + Expiration Date + Label for expiration date title + + + Card Number + Card Number + Label for card number title + + + Day + Day + Label for Day title + + + Deactivate + Deactivate + Deactivate button + + + Deactivate Duck Address + Deactivate Duck Address + Deactivate private email address button + + + Delete + Delete + Delete button + + + Edit + Edit + Edit button + + + Email Address + Email Address + Label for email address title + + + No Cards + No Cards + Label for cards empty state title + + + If your passwords are saved in another browser, you can import them into DuckDuckGo. + If your passwords are saved in another browser, you can import them into DuckDuckGo. + Label for default empty state description + + + No passwords or credit cards saved yet + No passwords or credit cards saved yet + Label for default empty state title + + + No Identities + No Identities + Label for identities empty state title + + + No passwords + No passwords + Label for logins empty state title + + + No Notes + No Notes + Label for notes empty state title + + + Enable Email Protection + Enable Email Protection + Text link to email protection website + + + Identification + Identification + Label for identification title + + + Address + Address + Default title for Addresses/Identities + + + Last Updated + Last Updated + Label for last updated edit field + + + Your autofill info will remain unlocked until your computer is idle for %@. + Your autofill info will remain unlocked until your computer is idle for %@. + Message about the duration for which autofill information remains unlocked on the lock screen. + + + Change in + Change in + Label used for a button that opens preferences + + + Settings + Settings + Label used for a button that opens preferences + + + unlock access to your autofill info + unlock access to your autofill info + Label presented when autofilling credit card information + + + change your autofill info access settings + change your autofill info access settings + Label presented when changing Auto-Lock settings + + + export your usernames and passwords + export your usernames and passwords + Label presented when exporting logins + + + unlock access to your autofill info + unlock access to your autofill info + Label presented when unlocking Autofill + + + 1 hour + 1 hour + Label used when selecting the Auto-Lock threshold + + + 1 minute + 1 minute + Label used when selecting the Auto-Lock threshold + + + 5 minutes + 5 minutes + Label used when selecting the Auto-Lock threshold + + + 12 hours + 12 hours + Label used when selecting the Auto-Lock threshold + + + 15 minutes + 15 minutes + Label used when selecting the Auto-Lock threshold + + + 30 minutes + 30 minutes + Label used when selecting the Auto-Lock threshold + + + Month + Month + Label for Month title + + + First Name + First Name + Label for first name title + + + Last Name + Last Name + Label for last name title + + + Middle Name + Middle Name + Label for middle name title + + + Credit Card + Credit Card + Label for new card title + + + Identity + Identity + Label for new identity title + + + Password + Password + Label for new login title + + + Note + Note + Label for new note title + + + Note + Note + Label for note title + + + Empty note + Empty note + Label for empty note title + + + Notes + Notes + Label for notes edit field + + + Password + Password + Label for password edit field + + + Phone Number + Phone Number + Label for phone number title + + + Emails sent to %@ will again be forwarded to your inbox. + Emails sent to %@ will again be forwarded to your inbox. + Text for the confirmation message displayed when a user tries activate a Private Email Address + + + Reactivate Private Duck Address? + Reactivate Private Duck Address? + Title for the confirmation message displayed when a user tries activate a Private Email Address + + + Duck Address Active + Duck Address Active + Mesasage displayed when a private email address is active + + + Emails sent to %@ will no longer be forwarded to your inbox. + Emails sent to %@ will no longer be forwarded to your inbox. + Text for the confirmation message displayed when a user tries deactivate a Private Email Address + + + Deactivate Private Duck Address? + Deactivate Private Duck Address? + Title for the confirmation message displayed when a user tries deactivate a Private Email Address + + + Management of this address is temporarily unavailable. + Management of this address is temporarily unavailable. + Mesasage displayed when a user tries to manage a private email address but the service is not available, returns an error or network is down + + + Duck Address Deactivated + Duck Address Deactivated + Mesasage displayed when a private email address is inactive + + + Got it + Got it + Button text for the alert dialog telling the user an updated username is no longer a private email address + + + You can still manage this Duck Address from emails received from it in your personal inbox. + You can still manage this Duck Address from emails received from it in your personal inbox. + Content for the alert dialog telling the user an updated username is no longer a private email address + + + Private Duck Address username was removed + Private Duck Address username was removed + Title for the alert dialog telling the user an updated username is no longer a private email address + + + Save + Save + Save button + + + Save password? + Save password? + Title for the editable Save Credentials popover + + + New Password Saved + New Password Saved + Title for the non-editable Save Credentials popover + + + %@ to manage your Duck Addresses on this device. + %@ to manage your Duck Addresses on this device. + Message displayed to the user when they are logged out of Email protection. + + + Newest First + Newest First + Label for Ascending date sort order + + + Oldest First + Oldest First + Label for Descending date sort order + + + Date Created + Date Created + Label for Date Created sort parameter + + + Date Modified + Date Modified + Label for Date Modified sort parameter + + + Title + Title + Label for Title sort parameter + + + Alphabetically + Alphabetically + Label for Ascending string sort order + + + Reverse Alphabetically + Reverse Alphabetically + Label for Descending string sort order + + + Username + Username + Label for username edit field + + + Website URL + Website URL + Label for website edit field + + + Year + Year + Label for Year title + + + Address: + Address: + Homepage address field label + + + Specific page + Specific page + Option to control Specific Home Page + + + New Tab page + New Tab page + Option to open a new tab + + + Set Homepage + Set Homepage + Set Homepage dialog title + + + Set Page… + Set Page… + Option to control the Specific Page + + + When navigating home or opening new windows. + When navigating home or opening new windows. + Homepage behavior description + + + Homepage + Homepage + Title for Homepage section in settings + + + About + About + Show about screen + + + About DuckDuckGo + About DuckDuckGo + About screen + + + More at %@ + More at %@ + Link to the about page + + + Privacy Policy + Privacy Policy + Link to privacy policy page + + + Privacy, simplified. + Privacy, simplified. + About screen + + + Send Feedback + Send Feedback + Feedback button in the about preferences page + + + DuckDuckGo is no longer providing browser updates for your version of macOS. + DuckDuckGo is no longer providing browser updates for your version of macOS. + This string represents a message informing the user that DuckDuckGo is no longer providing browser updates for their version of macOS + + + Please update to macOS %@ or later to use the most recent version of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates. + Please update to macOS %@ or later to use the most recent version of DuckDuckGo. You can also keep using your current version of the browser, but it will not receive further updates. + Copy in section that tells the user to update their macOS version since their current version is unsupported + + + Appearance + Appearance + Show appearance preferences + + + Address Bar + Address Bar + Theme preferences + + + Autocomplete suggestions + Autocomplete suggestions + Option to show autocomplete suggestions in the address bar + + + Full website address + Full website address + Option to show full URL in the address bar + + + Theme + Theme + Theme preferences + + + Dark + Dark + In the preferences for themes, the option to select for activating dark mode in the app. + + + Light + Light + In the preferences for themes, the option to select for activating light mode in the app. + + + System + System + In the preferences for themes, the option to select for use the change the mode based on the system preferences. + + + Zoom + Zoom + Zoom settings section title + + + Default page zoom + Default page zoom + Default page zoom picker title + + + Autofill + Autofill + Show Autofill preferences + + + Default Browser + Default Browser + Show default browser preferences + + + DuckDuckGo is your default browser + DuckDuckGo is your default browser + Indicate that the browser is the default + + + Make DuckDuckGo Default… + Make DuckDuckGo Default… + represents a prompt message asking the user to make DuckDuckGo their default browser. + + + DuckDuckGo is not your default browser. + DuckDuckGo is not your default browser. + Indicate that the browser is not the default + + + Downloads + Downloads + Show downloads browser preferences + + + Duck Player + Duck Player + Show Duck Player browser preferences + + + General + General + Show general preferences + + + On Startup + On Startup + Name of the preferences section related to app startup + + + Privacy + Privacy + Show privacy browser preferences + + + Reopen all windows from last session + Reopen all windows from last session + Option to control session restoration + + + Open a new window + Open a new window + Option to control session startup + + + Sync & Backup + Sync & Backup + Show sync preferences + + + Unlock device to setup Sync & Backup + Unlock device to setup Sync & Backup + Reason for auth when setting up Sync + + + VPN + VPN + Show VPN preferences + + + Print… + Print… + Menu item title + + + Quit + Quit + Quit button + + + Reload Page + Reload Page + Context menu item + + + Remove Favorite + Remove Favorite + Remove Favorite button + + + Remove from Favorites + Remove from Favorites + Button for removing bookmarks from favorites + + + Reopen Last Closed Tab + Reopen Last Closed Tab + This string represents an action to reopen the last closed tab in the browser + + + Reopen Last Closed Window + Reopen Last Closed Window + This string represents an action to reopen the last closed window in the browser + + + Report Broken Site + Report Broken Site + Menu with feedback commands + + + Restart Bitwarden + Restart Bitwarden + Button to restart Bitwarden application + + + Bitwarden is not responding. Please restart it to initiate the communication again + Bitwarden is not responding. Please restart it to initiate the communication again + This string represents a message informing the user that Bitwarden is not responding and prompts them to restart the application to initiate communication again. + + + Save + Save + Save button + + + Save Image As… + Save Image As… + Context menu item + + + Scroll to find the App Settings (All Accounts) section. + Scroll to find the App Settings (All Accounts) section. + Setup of the integration with Bitwarden app + + + Search with DuckDuckGo + Search with DuckDuckGo + Context menu item + + + Select Bitwarden → Preferences from the Mac menu bar. + Select Bitwarden → Preferences from the Mac menu bar. + Setup of the integration with Bitwarden app (up to and including macOS 12) + + + Select Bitwarden → Settings from the Mac menu bar. + Select Bitwarden → Settings from the Mac menu bar. + Setup of the integration with Bitwarden app (macOS 13 and above) + + + Send Browser Feedback + Send Browser Feedback + Menu with feedback commands + + + Your feedback will help us improve the DuckDuckGo app. + Your feedback will help us improve the DuckDuckGo app. + Text shown to the user when they provide feedback. + + + General feedback + General feedback + Name of the option the user can chose to give general browser feedback + + + Report a problem + Report a problem + Name of the option the user can chose to give browser feedback about a problem they enountered + + + Request a feature + Request a feature + Name of the option the user can chose to give browser feedback about a feature they would like + + + Select a category + Select a category + Title of the picker where the user can chose the category of the feedback they want ot send. + + + Thank you! + Thank you! + Thanks the user for sending feedback + + + Help Improve the DuckDuckGo Browser + Help Improve the DuckDuckGo Browser + Title of the interface to send feedback on the browser + + + Settings + Settings + Menu item for opening settings + + + Share + Share + Menu item title + + + Create QR Code + Create QR Code + Menu item title + + + More… + More… + Sharing Menu -> More… + + + Show Folder Contents + Show Folder Contents + Menu item that shows the content of a folder + + + Submit + Submit + Submit button + + + Submit Report + Submit Report + Submit Report button + + + Bookmarks + Bookmarks + Tab bookmarks title + + + Failed to open page + Failed to open page + Tab error title + + + New Tab + New Tab + Tab home title + + + Welcome + Welcome + Tab onboarding title + + + Settings + Settings + Tab preferences title + + + Add to Favorites + Add to Favorites + Tooltip for add to favorites button + + + Open application menu + Open application menu + Tooltip for the Application Menu button + + + Add item + Add item + Tooltip for the Add Item button + + + More options + More options + Tooltip for the More Options button + + + Autofill + Autofill + Tooltip for the autofill shortcut + + + Bookmark this page + Bookmark this page + Tooltip for the Add Bookmark button + + + Edit bookmark + Edit bookmark + Tooltip for the Edit Bookmark button + + + Manage bookmarks + Manage bookmarks + Tooltip for the Manage Bookmarks button + + + New bookmark + New bookmark + Tooltip for the New Bookmark button + + + New folder + New folder + Tooltip for the New Folder button + + + Bookmarks + Bookmarks + Tooltip for the bookmarks shortcut + + + Clear browsing history for %@ + Clear browsing history for %@ + Tooltip for burn button where %@ is the domain + + + Clear browsing history and data for %@ + Clear browsing history and data for %@ + Tooltip for burn button where %@ is the domain + + + Clear download history + Clear download history + Tooltip for the Clear Downloads button + + + Open downloads folder + Open downloads folder + Tooltip for the Open Downloads Folder button + + + Downloads + Downloads + Tooltip for the downloads shortcut + + + Close find bar + Close find bar + Tooltip for the Find In Page bar's Close button + + + Next result + Next result + Tooltip for the Find In Page bar's Next button + + + Previous result + Previous result + Tooltip for the Find In Page bar's Previous button + + + Clear browsing history + Clear browsing history + Tooltip for the Fire button + + + Home + Home + Tooltip for the home button + + + Show the previous page +Hold to show history + Show the previous page +Hold to show history + Tooltip for the Back button + + + Show the next page +Hold to show history + Show the next page +Hold to show history + Tooltip for the Forward button + + + Reload this page + Reload this page + Tooltip for the Refresh button + + + Stop loading this page + Stop loading this page + Tooltip for the Stop Navigation button + + + Show the Privacy Dashboard and manage site settings + Show the Privacy Dashboard and manage site settings + Tooltip for the Privacy Dashboard button + + + Open a new tab + Open a new tab + Tooltip for the New Tab button + + + Uninstall + Uninstall + Uninstall button + + + Unmute Tab + Unmute Tab + Menu item. Unmute tab + + + Unpin Tab + Unpin Tab + Menu item. Unpin as a verb + + + Your version of macOS is no longer supported. + Your version of macOS is no longer supported. + his string represents the header for an alert informing the user that their version of macOS is no longer supported + + + Update + Update + Update button + + + Version %1$@ (%2$@) + Version %1$@ (%2$@) + Displays the version and build numbers + + + DuckDuckGo automatically blocks hidden trackers as you browse the web. + DuckDuckGo automatically blocks hidden trackers as you browse the web. + feature explanation in settings + + + Web Tracking Protection + Web Tracking Protection + Web tracking protection settings section title + + + Zoom + Zoom + Menu with Zooming commands + + + •••••••••••• + •••••••••••• + + + + ⚠️ Notes are deprecated. + ⚠️ Notes are deprecated. + + + +
+ +
+ +
+ + + (%@) + (%@) + + + + Sorry, this code is invalid. Please make sure it was entered correctly. + Sorry, this code is invalid. Please make sure it was entered correctly. + Description for invalid code error + + + Sync & Backup Error + Sync & Backup Error + Title for sync error alert + + + Unable to create the recovery PDF. + Unable to create the recovery PDF. + Description for unable to create recovery pdf error + + + Unable to delete data on the server. + Unable to delete data on the server. + Description for unable to delete data error + + + To pair these devices, turn off Sync & Backup on one device then tap "Sync With Another Device" on the other device. + To pair these devices, turn off Sync & Backup on one device then tap "Sync With Another Device" on the other device. + Description for unable to merge two accounts error + + + Unable to remove this device from Sync & Backup. + Unable to remove this device from Sync & Backup. + Description for unable to remove device error + + + Unable to connect to the server. + Unable to connect to the server. + Description for unable to sync to server error + + + Unable to Sync with another device. + Unable to Sync with another device. + Description for unable to sync with another device error + + + Unable to turn Sync & Backup off. + Unable to turn Sync & Backup off. + Description for unable to turn sync off error + + + Unable to update the device name. + Unable to update the device name. + Description for unable to update device name error + + + Cancel + Cancel + Cancel button + + + Copy + Copy + Copy button + + + Done + Done + Done button + + + Next + Next + Next button + + + Not Now + Not Now + Not Now button + + + OK + OK + OK button + + + Paste + Paste + Paste button + + + Paste from Clipboard + Paste from Clipboard + Paste from Clipboard button + + + Sync With Another Device + Sync With Another Device + Button text on the Begin Syncing card in sync settings + + + Securely sync bookmarks and passwords between your devices. + Securely sync bookmarks and passwords between your devices. + Begin Syncing card description in sync settings + + + Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key. + Your data is end-to-end encrypted, and DuckDuckGo does not have access to the encryption key. + Footer / caption on the Begin Syncing card in sync settings + + + Begin Syncing + Begin Syncing + Begin Syncing card title in sync settings + + + Paste Code Here + Paste Code Here + Sync enter recovery code dialog first possible action + + + or scan QR code with a device that is still connected + or scan QR code with a device that is still connected + Sync enter recovery code dialog second possible action + + + Enter the code on your Recovery PDF, or another synced device, to recover your synced data. + Enter the code on your Recovery PDF, or another synced device, to recover your synced data. + Sync enter recovery code dialog subtitle + + + Enter Code + Enter Code + Sync enter recovery code dialog title + + + Other Options + Other Options + Sync settings. Other Options section title + + + Connecting… + Connecting… + Sync preparing to sync dialog action + + + We're setting up the connection to synchronize your bookmarks and saved logins with the other device. + We're setting up the connection to synchronize your bookmarks and saved logins with the other device. + Preparing to sync dialog subtitle during sync set up + + + Preparing To Sync + Preparing To Sync + Preparing to sync dialog title during sync set up + + + Recover Synced Data + Recover Synced Data + Sync settings. Link to recover synced data. + + + Get Started + Get Started + Sync recover synced data dialog button + + + To restore your synced data, you'll need the Recovery Code you saved when you first set up Sync. This code may have been saved as a PDF on the device you originally used to set up Sync. + To restore your synced data, you'll need the Recovery Code you saved when you first set up Sync. This code may have been saved as a PDF on the device you originally used to set up Sync. + Recover synced data during Sync recovery process dialog subtitle + + + Recover Synced Data + Recover Synced Data + Sync recover synced data dialog title + + + Sync & Backup + Sync & Backup + Show sync preferences + + + Sync and Back Up This Device + Sync and Back Up This Device + Sync settings. Title of a link to start setting up sync and backup the device + + + Sync Enabled + Sync Enabled + Sync state is enabled + + + Details... + Details... + Sync Settings device details button + + + Remove... + Remove... + Button to remove a device + + + Remove Device + Remove Device + Button text on remove a device confirmation button + + + "%@" will no longer be able to access your synced data. + "%@" will no longer be able to access your synced data. + Message to confirm the device will no longer be able to access the synced data - devoce name item inserted + + + Remove device? + Remove device? + Title on remove a device confirmation + + + Sync & Backup is rolling out gradually and may not be available yet within DuckDuckGo on your other devices. + Sync & Backup is rolling out gradually and may not be available yet within DuckDuckGo on your other devices. + Description of rollout banner + + + Go to %@ in the DuckDuckGo Browser on another device and select Sync With Another Device. + Go to %@ in the DuckDuckGo Browser on another device and select Sync With Another Device. + Sync with another device dialog subtitle - Instruction with sync menu path item inserted + + + Sync With Another Device + Sync With Another Device + Sync with another device dialog title + + + Enter Code + Enter Code + Text on enter code button on Sync with another device dialog + + + Paste the code here to sync. + Paste the code here to sync. + Sync with another device dialog enter code explanation + + + Show Code + Show Code + Text on show code button on Sync with another device dialog + + + Share this code to connect with a desktop machine. + Share this code to connect with a desktop machine. + Sync with another device dialog show code explanation + + + Scan this QR code to connect. + Scan this QR code to connect. + Sync with another device dialog show qr code explanation + + + View QR Code + View QR Code + Sync with another device dialog view qr code link + + + View Text Code + View Text Code + Sync with another device dialog view text code link + + + Turn On Sync & Backup + Turn On Sync & Backup + Sync with server dialog button + + + This creates an encrypted backup of your bookmarks and passwords on DuckDuckGo’s secure server, which can be synced with your other devices. + This creates an encrypted backup of your bookmarks and passwords on DuckDuckGo’s secure server, which can be synced with your other devices. + Sync with server dialog first subtitle + + + The encryption key is only stored on your device, DuckDuckGo cannot access it. + The encryption key is only stored on your device, DuckDuckGo cannot access it. + Sync with server dialog second subtitle + + + Sync and Back Up This Device + Sync and Back Up This Device + Sync with server dialog title + + + Synced Devices + Synced Devices + Settings section title + + + This Device + This Device + Indicator of a current user's device on the list + + + Turn Off + Turn Off + Turn off sync confirmation dialog button title + + + Turn Off and Delete Server Data… + Turn Off and Delete Server Data… + Disable and delete data sync button caption + + + This device will no longer be able to access your synced data. + This device will no longer be able to access your synced data. + Turn off sync confirmation dialog message + + + Turn off sync? + Turn off sync? + Turn off sync confirmation dialog title + + + Turn Off Sync… + Turn Off Sync… + Disable sync button caption + + + Manage Bookmarks + Manage Bookmarks + Button title for sync bookmarks limits exceeded warning to go to manage bookmarks + + + Bookmark limit exceeded. Delete some to resume syncing. + Bookmark limit exceeded. Delete some to resume syncing. + Description for sync bookmarks limits exceeded warning + + + Manage passwords… + Manage passwords… + Button title for sync credentials limits exceeded warning to go to manage passwords + + + Logins limit exceeded. Delete some to resume syncing. + Logins limit exceeded. Delete some to resume syncing. + Description for sync credentials limits exceeded warning + + + Delete Data + Delete Data + Label for delete account button + + + These devices will be disconnected and your synced data will be deleted from the server. + These devices will be disconnected and your synced data will be deleted from the server. + Message for delete account confirmation pop up + + + Delete server data? + Delete server data? + Title for delete account confirmation pop up + + + Name + Name + The text entry label to name the device + + + Device name + Device name + The text entry prompt to name the device + + + Device Details + Device Details + The title of the device details dialog + + + Your data is synced! + Your data is synced! + Sync setup confirmation dialog title + + + Do you want this device to automatically download icons for any new bookmarks synced from your other devices? This will expose the download to your network any time a bookmark is synced. + Do you want this device to automatically download icons for any new bookmarks synced from your other devices? This will expose the download to your network any time a bookmark is synced. + Text for fetch favicons onboarding dialog + + + Download Missing Icons? + Download Missing Icons? + Title for fetch favicons onboarding dialog + + + Automatically download icons for synced bookmarks. Icon downloads are exposed to your network. + Automatically download icons for synced bookmarks. Icon downloads are exposed to your network. + Caption for fetch favicons option + + + Auto-Download Icons + Auto-Download Icons + Title for fetch favicons option + + + Keep Bookmarks Icons Updated + Keep Bookmarks Icons Updated + Title of the confirmation button for favicons fetching + + + Sync Paused + Sync Paused + Title for sync limits exceeded warning + + + Options + Options + Title for options settings + + + Recovery + Recovery + Sync settings section title + + + If you lose your device, you will need this recovery code to restore your synced data. + If you lose your device, you will need this recovery code to restore your synced data. + Instructions on how to restore synced data + + + Copy Code + Copy Code + Sync recovery PDF copy code button + + + If you lose access to your devices, you will need this code to recover your synced data. You can save this code to your device as a PDF. + If you lose access to your devices, you will need this code to recover your synced data. You can save this code to your device as a PDF. + Sync recovery PDF explanation + + + Save PDF + Save PDF + Sync recovery PDF save pdf button + + + Anyone with access to this code can access your synced data, so please keep it in a safe place. + Anyone with access to this code can access your synced data, so please keep it in a safe place. + Sync recovery PDF warning + + + Save Your Recovery Code + Save Your Recovery Code + Caption for a button to save Sync recovery PDF + + + Use the same favorite bookmarks on all your devices. Leave off to keep mobile and desktop favorites separate. + Use the same favorite bookmarks on all your devices. Leave off to keep mobile and desktop favorites separate. + Caption for share favorite option + + + Unify Favorites Across Devices + Unify Favorites Across Devices + Title for share favorite option + + + Share + Share + Share button + + + Submit + Submit + Submit button + + + Settings › Sync & Backup + Settings › Sync & Backup + Sync Menu Path + + + Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue. + Sorry, but Sync & Backup is no longer available in this app version. Please update DuckDuckGo to the latest version to continue. + Data syncing unavailable warning message + + + Sync & Backup is Paused + Sync & Backup is Paused + Title of the warning message that Sync & Backup is Paused + + + Sync & Backup is Unavailable + Sync & Backup is Unavailable + Title of the warning message that sync and backup are unavailable + + + Sorry, but Sync & Backup is currently unavailable. Please try again later. + Sorry, but Sync & Backup is currently unavailable. Please try again later. + Data syncing unavailable warning message + + +
+