From 60df7568dc2fab96109ce5f95d83bebe35378207 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:38:04 +0200 Subject: [PATCH] onboarding dax dialogs (#3149) Task/Issue URL: https://app.asana.com/0/1204186595873227/1208077416568671/f Tech Design URL: Look at BSK branch **Description**: Adds Contextual Dax Dialogs --- DuckDuckGo.xcodeproj/project.pbxproj | 42 + DuckDuckGo/Common/Localizables/UserText.swift | 24 + DuckDuckGo/Localizable.xcstrings | 1200 +++++++++++++++++ .../ContextualOnboardingDialogs.swift | 278 ++++ .../OnboardingSuggestedSearchesProvider.swift | 79 ++ ...ardingSuggestedSearchesProviderTests.swift | 77 ++ 6 files changed, 1700 insertions(+) create mode 100644 DuckDuckGo/Onboarding/ContextualOnboarding/ContextualOnboardingDialogs.swift create mode 100644 DuckDuckGo/Onboarding/ContextualOnboarding/OnboardingSuggestedSearchesProvider.swift create mode 100644 UnitTests/Onboarding/OnboardingSuggestedSearchesProviderTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 35783aeca4..a94f9351cc 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1428,6 +1428,12 @@ 560C3FFD2BC9911000F589CE /* PermanentSurveyManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560C3FFB2BC9911000F589CE /* PermanentSurveyManagerTests.swift */; }; 560C3FFF2BCD5A1E00F589CE /* PermanentSurveyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560C3FFE2BCD5A1E00F589CE /* PermanentSurveyManager.swift */; }; 560C40002BCD5A1E00F589CE /* PermanentSurveyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560C3FFE2BCD5A1E00F589CE /* PermanentSurveyManager.swift */; }; + 560EB9322C78946F0080DBC8 /* ContextualOnboardingDialogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EB9312C78946F0080DBC8 /* ContextualOnboardingDialogs.swift */; }; + 560EB9332C78946F0080DBC8 /* ContextualOnboardingDialogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EB9312C78946F0080DBC8 /* ContextualOnboardingDialogs.swift */; }; + 560EB9352C7897370080DBC8 /* Onboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 560EB9342C7897370080DBC8 /* Onboarding */; }; + 560EB9372C78974C0080DBC8 /* Onboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 560EB9362C78974C0080DBC8 /* Onboarding */; }; + 560EB9392C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EB9382C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift */; }; + 560EB93A2C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EB9382C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift */; }; 5614B3A12BBD639D009B5031 /* ZoomPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5614B3A02BBD639D009B5031 /* ZoomPopover.swift */; }; 5614B3A22BBD639D009B5031 /* ZoomPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5614B3A02BBD639D009B5031 /* ZoomPopover.swift */; }; 561D29C22BDA745A007B91D0 /* MockSyncPausedStateManaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561D29C02BDA7430007B91D0 /* MockSyncPausedStateManaging.swift */; }; @@ -1517,6 +1523,8 @@ 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 */; }; + 56CE77612C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CE77602C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift */; }; + 56CE77622C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CE77602C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift */; }; 56CEE90E2B7A725B00CF10AA /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */; }; 56CEE90F2B7A725C00CF10AA /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */; }; 56D145E829E6BB6300E3488A /* CapturingDataImportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D145E729E6BB6300E3488A /* CapturingDataImportProvider.swift */; }; @@ -3491,6 +3499,8 @@ 5603D90529B7B746007F9F01 /* MockTabViewItemDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTabViewItemDelegate.swift; sourceTree = ""; }; 560C3FFB2BC9911000F589CE /* PermanentSurveyManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermanentSurveyManagerTests.swift; sourceTree = ""; }; 560C3FFE2BCD5A1E00F589CE /* PermanentSurveyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermanentSurveyManager.swift; sourceTree = ""; }; + 560EB9312C78946F0080DBC8 /* ContextualOnboardingDialogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingDialogs.swift; sourceTree = ""; }; + 560EB9382C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSuggestedSearchesProvider.swift; sourceTree = ""; }; 5614B3A02BBD639D009B5031 /* ZoomPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomPopover.swift; sourceTree = ""; }; 561D29C02BDA7430007B91D0 /* MockSyncPausedStateManaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSyncPausedStateManaging.swift; sourceTree = ""; }; 561D29C42BDA749A007B91D0 /* MockDDGSyncing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDDGSyncing.swift; sourceTree = ""; }; @@ -3537,6 +3547,7 @@ 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 = ""; }; + 56CE77602C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSuggestedSearchesProviderTests.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 = ""; }; @@ -4400,6 +4411,7 @@ 3706FCAA293F65D500E42796 /* UserScript in Frameworks */, 85E2BBD02B8F534A00DBEC7A /* History in Frameworks */, 4BF97AD52B43C43F00EB4240 /* NetworkProtection in Frameworks */, + 560EB9372C78974C0080DBC8 /* Onboarding in Frameworks */, 3739326529AE4B39009346AE /* DDGSync in Frameworks */, D6BC8AC82C5A95B10025375B /* DuckPlayer in Frameworks */, 37DF000729F9C061002B7D3E /* SyncDataProviders in Frameworks */, @@ -4586,6 +4598,7 @@ files = ( C18BF9CC2C73678500ED6B8A /* Freemium in Frameworks */, F1DF95E32BD1807C0045E591 /* Crashes in Frameworks */, + 560EB9352C7897370080DBC8 /* Onboarding in Frameworks */, 85E2BBCE2B8F534000DBEC7A /* History in Frameworks */, 1EA7B8D32B7E078C000330A4 /* SubscriptionUI in Frameworks */, B6F7128129F681EB00594A45 /* QuickLookUI.framework in Frameworks */, @@ -6079,6 +6092,15 @@ path = View; sourceTree = ""; }; + 560EB9302C78943E0080DBC8 /* ContextualOnboarding */ = { + isa = PBXGroup; + children = ( + 560EB9312C78946F0080DBC8 /* ContextualOnboardingDialogs.swift */, + 560EB9382C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift */, + ); + path = ContextualOnboarding; + sourceTree = ""; + }; 561D29BF2BDA7419007B91D0 /* Mocks */ = { isa = PBXGroup; children = ( @@ -6496,6 +6518,7 @@ 85B7184727677A7D00B4277F /* Onboarding */ = { isa = PBXGroup; children = ( + 560EB9302C78943E0080DBC8 /* ContextualOnboarding */, 85707F2F276A7DB000DC0649 /* ViewModel */, 85B7184827677A9200B4277F /* View */, 56A053FB2C19E8F7007D8FAB /* OnboardingActionsManager.swift */, @@ -6608,6 +6631,7 @@ 56A0542F2C2043C8007D8FAB /* OnboardingTabExtensionTests.swift */, 56A054402C22438C007D8FAB /* OnboardingNavigatingTests.swift */, 56A054432C2252CE007D8FAB /* OnboardingUserScriptTests.swift */, + 56CE77602C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift */, ); path = Onboarding; sourceTree = ""; @@ -8745,6 +8769,7 @@ 371209242C232E6C003ADF3D /* RemoteMessaging */, D6BC8AC72C5A95B10025375B /* DuckPlayer */, 9D9DE5742C63AA0C00D20B15 /* AppKitExtensions */, + 560EB9362C78974C0080DBC8 /* Onboarding */, C18BF9CD2C73678C00ED6B8A /* Freemium */, ); productName = DuckDuckGo; @@ -9162,6 +9187,7 @@ 371209222C232E66003ADF3D /* RemoteMessaging */, D6BC8AC52C5A95AA0025375B /* DuckPlayer */, 9D9DE5722C63AA0700D20B15 /* AppKitExtensions */, + 560EB9342C7897370080DBC8 /* Onboarding */, C18BF9CB2C73678500ED6B8A /* Freemium */, ); productName = DuckDuckGo; @@ -10043,6 +10069,7 @@ 3706FAB9293F65D500E42796 /* TabBarViewController.swift in Sources */, 56A054202C1CA1F5007D8FAB /* OnboardingTabExtension.swift in Sources */, 3706FABA293F65D500E42796 /* BookmarkOutlineViewDataSource.swift in Sources */, + 560EB9332C78946F0080DBC8 /* ContextualOnboardingDialogs.swift in Sources */, 3706FABB293F65D500E42796 /* PasswordManagementBitwardenItemView.swift in Sources */, 1D220BF92B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, 9FA173E42B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, @@ -10649,6 +10676,7 @@ 3706FC47293F65D500E42796 /* FavoritesView.swift in Sources */, 3706FC48293F65D500E42796 /* HomePage.swift in Sources */, 56A0543F2C215FB3007D8FAB /* OnboardingUserScript.swift in Sources */, + 560EB93A2C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift in Sources */, 3706FC49293F65D500E42796 /* RoundedSelectionRowView.swift in Sources */, 4B9DB01E2A983B24000927DB /* Waitlist.swift in Sources */, 3706FC4A293F65D500E42796 /* LocalStatisticsStore.swift in Sources */, @@ -10864,6 +10892,7 @@ 3706FE03293F661700E42796 /* CoreDataStoreTests.swift in Sources */, 1D9FDEC42B9B63C90040B78C /* DataClearingPreferencesTests.swift in Sources */, 3706FE04293F661700E42796 /* TreeControllerTests.swift in Sources */, + 56CE77622C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */, 3706FE05293F661700E42796 /* DownloadsWebViewMock.m in Sources */, 3706FE06293F661700E42796 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, 56BA1E7F2BAB2D29001CF69F /* ErrorPageTabExtensionTest.swift in Sources */, @@ -11668,6 +11697,7 @@ B6AAAC2D260330580029438D /* PublishedAfter.swift in Sources */, 3701C9CE29BD040C00305B15 /* FirefoxBerkeleyDatabaseReader.swift in Sources */, 37054FCE2876472D00033B6F /* WebViewSnapshotView.swift in Sources */, + 560EB9392C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift in Sources */, 4BBC16A027C4859400E00A38 /* DeviceAuthenticationService.swift in Sources */, CB24F70C29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */, 1DEF3BAD2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */, @@ -11928,6 +11958,7 @@ C13909EF2B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */, 4BB88B5B25B7BA50006F6B06 /* Instruments.swift in Sources */, 3768D8442C2CC884004120AE /* RemoteMessagingConfigMatcherProvider.swift in Sources */, + 560EB9322C78946F0080DBC8 /* ContextualOnboardingDialogs.swift in Sources */, 9812D895276CEDA5004B6181 /* ContentBlockerRulesLists.swift in Sources */, 4B0511E2262CAA8600F6079C /* NSViewControllerExtension.swift in Sources */, C16127EE2BDFB46400966BB9 /* DataImportShortcutsView.swift in Sources */, @@ -12295,6 +12326,7 @@ 4B9292BB2667103100AD2C21 /* BookmarkNodeTests.swift in Sources */, 4B0219A825E0646500ED7DEA /* WebsiteDataStoreTests.swift in Sources */, AAC9C01E24CB6BEB00AD1325 /* TabCollectionViewModelTests.swift in Sources */, + 56CE77612C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */, B662D3DE275613BB0035D4D6 /* EncryptionKeyStoreMock.swift in Sources */, 1D3B1ABF29369FC8006F4388 /* BWEncryptionTests.swift in Sources */, B6F56567299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, @@ -13893,6 +13925,16 @@ package = 4311906792B7676CE9535D76 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Crashes; }; + 560EB9342C7897370080DBC8 /* Onboarding */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Onboarding; + }; + 560EB9362C78974C0080DBC8 /* Onboarding */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Onboarding; + }; 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionProxy; diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index b2dfd2c47c..0378703b02 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1246,6 +1246,30 @@ struct UserText { } } } + + // MARK: - Onboarding + enum ContextualOnboarding { + static let onboardingTryASearchTitle = NSLocalizedString("contextual.onboarding.try-a-search.title", value: "Try a search!", comment: "Title of a popover on the browser that invites the user to try a search") + static let onboardingTryASearchMessage = NSLocalizedString("contextual.onboarding.try-a-search.message", value: "Your DuckDuckGo searches are always anonymous.", comment: "Message of a popover on the browser that invites the user to try a search explaining that their searches are anonymous") + static let onboardingTryASiteTitle = NSLocalizedString("contextual.onboarding.try-a-site.title", value: "Next, try visiting a site!", comment: "Title of a popover on the browser that invites the user to try a visiting a website") + static let onboardingTryASiteNTPTitle = NSLocalizedString("contextual.onboarding.ntp.try-a-site.title", value: "Try visiting a site!", comment: "Title of a popover on the new tab page browser that invites the user to try a visiting a website") + static let onboardingTryASiteMessage = NSLocalizedString("contextual.onboarding.try-a-site.message", value: "I’ll block trackers so they can’t spy on you.", comment: "Message of a popover on the browser that invites the user to try visiting a website to explain that we block trackers") + static let onboardingTryFireButtonMessage = NSLocalizedString("contextual.onboarding.try-fire-button.message", value: "Instantly clear your browsing activity with the Fire Button.\n\nGive it a try! 🔥", comment: "Message of a popover on the browser that invites the user to try visiting the browser Fire Button. Please leave the line break") + static let onboardingGotItButton = NSLocalizedString("contextual.onboarding.got-it.button", value: "Got it", comment: "During onboarding steps this button is shown and takes either to the next steps or closes the onboarding.") + static let onboardingFirstSearchDoneTitle = NSLocalizedString("contextual.onboarding.first-search-done.title", value: "That’s DuckDuckGo Search.", comment: "After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo") + static let onboardingFirstSearchDoneMessage = NSLocalizedString("contextual.onboarding.first-search-done.message", value: "Private. Fast. Fewer ads.", comment: "After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo") + static let onboardingFinalScreenTitle = NSLocalizedString("contextual.onboarding.final-screen.title", value: "You’ve got this!", comment: "Title of the last screen of the onboarding to the browser app") + static let onboardingFinalScreenMessage = NSLocalizedString("contextual.onboarding.final-screen.message", value: "Remember: every time you browse with me a creepy ad loses its wings. 👌", comment: "Message of the last screen of the onboarding to the browser app.") + static let onboardingFinalScreenButton = NSLocalizedString("contextual.onboarding.final-screen.button", value: "High five!", comment: "Button on the last screen of the onboarding, it will dismiss the onboarding screen.") + static let tryASearchOption1English = NSLocalizedString("contextual.onboarding.try-search.option1-English", value: "how to say “duck” in spanish", comment: "Browser Search query for how to say duck in english") + static let tryASearchOption1International = NSLocalizedString("contextual.onboarding.try-search.option1international", value: "how to say “duck” in english", comment: "Browser Search query for how to say duck in english") + static let tryASearchOption2English = NSLocalizedString("contextual.onboarding.try-search.option2-english", value: "mighty ducks cast", comment: "Search query for the cast of Mighty Ducks") + static let tryASearchOption2International = NSLocalizedString("contextual.onboarding.try-search.option2-international", value: "cast of avatar", comment: "Search query for the cast of Avatar") + static let tryASearchOption3 = NSLocalizedString("contextual.onboarding.try-search.option3", value: "local weather", comment: "Browser Search query for local weather") + static let tryASearchOptionSurpriseMeTitle = NSLocalizedString("contextual.onboarding.try-search.surprise-me-title", value: "Surprise me!", comment: "Title for a button that triggers an unknown search query for the user.") + static let tryASearchOptionSurpriseMeEnglish = NSLocalizedString("contextual.onboarding.try-search.surprise-me-english", value: "chocolate chip cookie recipes", comment: "Browser Search query for chocolate chip cookie recipes") + static let tryASearchOptionSurpriseMeInternational = NSLocalizedString("contextual.onboarding.try-search.surprise-me-international", value: "dinner recipes", comment: "Browser Search query for dinner recipes") + } // Key: "subscription.menu.item" // Comment: "Title for Subscription item in the options menu" diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index ccd1671697..374d3ae6f2 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -13390,6 +13390,1206 @@ } } }, + "contextual.onboarding.final-screen.button" : { + "comment" : "Button on the last screen of the onboarding, it will dismiss the onboarding screen.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schlag ein!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "High five!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Choca esos cinco!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bien joué !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Batti cinque!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "High five!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Piątka!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dá cá cinco!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дай пять!" + } + } + } + }, + "contextual.onboarding.final-screen.message" : { + "comment" : "Message of the last screen of the onboarding to the browser app.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hinweis: Jedes Mal, wenn du mit mir browst, verliert eine gruselige Anzeige ihren Schrecken. 👌" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Remember: every time you browse with me a creepy ad loses its wings. 👌" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recuerda: cada vez que navegas conmigo corto las alas a un anuncio horrible. 👌" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pensez-y : chaque fois que vous naviguez avec moi, une publicité douteuse disparaît. 👌" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ricorda: quando navighi con me gli annunci inquietanti non possono seguirti. 👌" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Denk eraan: elke keer als je met mij browset, verliest een enge advertentie zijn vleugels. 👌" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pamiętaj: za każdym razem, gdy przeglądasz ze mną Internet, jakaś wstrętna reklama przestaje działać. 👌" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lembra-te: sempre que navegas comigo, um anúncio assustador perde as suas asas. 👌" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Бродить по сайтам с нами — значит подрезать крылья назойливой рекламе. 👌" + } + } + } + }, + "contextual.onboarding.final-screen.title" : { + "comment" : "Title of the last screen of the onboarding to the browser app", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gut gemacht!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "You’ve got this!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Lo estás haciendo muy bien!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bien joué !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ben fatto!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je kunt het!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Udało się!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Você consegue!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проще некуда!" + } + } + } + }, + "contextual.onboarding.first-search-done.message" : { + "comment" : "After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privat. Schnell. Weniger Werbung." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Private. Fast. Fewer ads." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privado. Rápido. Menos anuncios." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privé. Rapide. Moins de publicités." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privato. Veloce. Meno annunci." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privé. Snel. Minder advertenties." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prywatna. Szybka. Z mniejszą liczbą reklam." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privado. Rápido. Menos anúncios." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надежно. Быстро. Меньше рекламы." + } + } + } + }, + "contextual.onboarding.first-search-done.title" : { + "comment" : "After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das ist DuckDuckGo Search." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "That’s DuckDuckGo Search." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eso es DuckDuckGo Search." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "C'est DuckDuckGo Search." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "È DuckDuckGo Search." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dat is DuckDuckGo Search." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "To DuckDuckGo Search." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "É a DuckDuckGo Search." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Это — DuckDuckGo Search." + } + } + } + }, + "contextual.onboarding.got-it.button" : { + "comment" : "During onboarding steps this button is shown and takes either to the next steps or closes the onboarding.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verstanden" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Got it" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entendido" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "J'ai compris" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ho capito" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ik snap het" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rozumiem" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entendi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Понятно" + } + } + } + }, + "contextual.onboarding.ntp.try-a-site.title" : { + "comment" : "Title of a popover on the new tab page browser that invites the user to try a visiting a website", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versuche, eine Website zu besuchen!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Try visiting a site!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Intenta visitar un sitio!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Essayez de visiter un site !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prova a visitare un sito!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bezoek eens een site!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spróbuj odwiedzić witrynę!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Experimenta visitar um site!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Попробуйте посетить сайт!" + } + } + } + }, + "contextual.onboarding.try-a-search.message" : { + "comment" : "Message of a popover on the browser that invites the user to try a search explaining that their searches are anonymous", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine DuckDuckGo-Suchanfragen sind immer anonym." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Your DuckDuckGo searches are always anonymous." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tus búsquedas en DuckDuckGo son siempre anónimas." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vos recherches sur DuckDuckGo sont toujours anonymes." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le tue ricerche su DuckDuckGo sono sempre anonime." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je DuckDuckGo-zoekopdrachten zijn altijd anoniem." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyszukiwania w DuckDuckGo zawsze są anonimowe." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "As tuas pesquisas no DuckDuckGo são sempre anónimas." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваши поисковые запросы в DuckDuckGo всегда анонимны." + } + } + } + }, + "contextual.onboarding.try-a-search.title" : { + "comment" : "Title of a popover on the browser that invites the user to try a search", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Probiere eine Suche aus!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Try a search!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Prueba con una búsqueda!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Essayez une recherche !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prova una ricerca!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Probeer een zoekopdracht!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spróbuj coś wyszukać!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Experimenta fazer uma pesquisa!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Попробуйте ввести запрос!" + } + } + } + }, + "contextual.onboarding.try-a-site.message" : { + "comment" : "Message of a popover on the browser that invites the user to try visiting a website to explain that we block trackers", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ich blockiere Tracker, damit sie dich nicht ausspionieren können." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "I’ll block trackers so they can’t spy on you." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloquearé los rastreadores para que no puedan espiarte." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je bloquerai les traqueurs afin qu'ils ne puissent pas vous espionner." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloccherò i sistemi di tracciamento in modo che non possano spiarti e" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ik blokkeer trackers zodat ze je niet kunnen bespioneren." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zablokuję skrypty śledzące, aby nie mogły Cię szpiegować." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloquearei rastreadores para que não possam espiá-lo." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мы заблокируем трекеры и пресечем слежку." + } + } + } + }, + "contextual.onboarding.try-a-site.title" : { + "comment" : "Title of a popover on the browser that invites the user to try a visiting a website", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versuche als nächstes, eine Website zu besuchen!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Next, try visiting a site!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡A continuación, intenta visitar un sitio!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ensuite, essayez de visiter un site !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "In seguito, prova a visitare un sito!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Probeer nu een site te bezoeken!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Następnie spróbuj odwiedzić witrynę!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Em seguida, experimenta visitar um site!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "А теперь попробуйте посетить сайт!" + } + } + } + }, + "contextual.onboarding.try-fire-button.message" : { + "comment" : "Message of a popover on the browser that invites the user to try visiting the browser Fire Button. Please leave the line break", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lösche deine Browseraktivitäten sofort mit dem Fire Button.\n\nProbier’s doch mal aus! 🔥" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Instantly clear your browsing activity with the Fire Button.\n\nGive it a try! 🔥" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borra al instante tu actividad de navegación con el Fire Button.\n\n¡Pruébalo! 🔥" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effacez instantanément votre activité de navigation avec le Fire Button.\n\nEssayez par vous-même ! 🔥" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancella istantaneamente la tua attività di navigazione con il Fire Button.\n\nProvalo! 🔥" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wis je browser-activiteit direct met de Fire Button.\n\nProbeer het maar! 🔥" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Natychmiast wyczyść swoją aktywność związaną z przeglądaniem za pomocą przycisku Fire Button.\n\nSpróbuj! 🔥" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpa instantaneamente a tua atividade de navegação com o Fire Button.\n\nExperimenta! 🔥" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Кнопка Fire Button моментально стирает из браузера данные о посещении сайтов.\n\nУбедитесь сами! 🔥" + } + } + } + }, + "contextual.onboarding.try-search.option1-English" : { + "comment" : "Browser Search query for how to say duck in english", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "wie sagt man „Ente“ auf Spanisch" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "how to say “duck” in spanish" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cómo se dice «pato» en inglés" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "comment dire « duck » en espagnol" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "come si dice \"anatra\" in spagnolo" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "hoe zeg je 'eend' in het Spaans?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "jak się mówi „kaczka” po hiszpańsku" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "como dizer \"pato\" em espanhol" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Как сказать «утка» по-испански?" + } + } + } + }, + "contextual.onboarding.try-search.option1international" : { + "comment" : "Browser Search query for how to say duck in english", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "wie sagt man „Ente“ auf Englisch" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "how to say “duck” in english" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cómo se dice «pato» en inglés" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "comment dire « canard » en anglais" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "come si dice \"anatra\" in inglese" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "hoe zeg je 'eend' in het Engels?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "jak się mówi „kaczka” po angielsku" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "como dizer \"pato\" em inglês" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Как сказать «утка» по-английски?" + } + } + } + }, + "contextual.onboarding.try-search.option2-english" : { + "comment" : "Search query for the cast of Mighty Ducks", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Besetzung von Mighty Ducks" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "mighty ducks cast" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "reparto de Mighty Ducks" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "casting de mighty ducks" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "cast delle papere potenti" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "cast van Mighty Ducks" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "obsada potężnych kaczorów" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "elenco do filme A Hora dos Campeões" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Кто играет главные роли в фильме «Могучие утята»?" + } + } + } + }, + "contextual.onboarding.try-search.option2-international" : { + "comment" : "Search query for the cast of Avatar", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Besetzung von Avatar" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "cast of avatar" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "reparto de Avatar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "casting d'avatar" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "cast di avatar" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "cast van avatar" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "obsada avatara" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "elenco de avatar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "актерский состав аватара" + } + } + } + }, + "contextual.onboarding.try-search.option3" : { + "comment" : "Browser Search query for local weather", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lokales Wetter" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "local weather" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "el tiempo local" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "météo locale" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "meteo locale" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "lokaal weer" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "pogoda lokalna" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "meteorologia local" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Местная погода" + } + } + } + }, + "contextual.onboarding.try-search.surprise-me-english" : { + "comment" : "Browser Search query for chocolate chip cookie recipes", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rezepte für Schokoladenkekse" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "chocolate chip cookie recipes" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "recetas de galletas con pepitas de chocolate" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "recettes de cookies aux pépites de chocolat" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "ricette di biscotti con gocce di cioccolato" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recepten voor chocoladekoekjes" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "przepisy na ciastka z kawałkami czekolady" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "receitas de biscoitos de chocolate" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "рецепты печенья с шоколадной крошкой" + } + } + } + }, + "contextual.onboarding.try-search.surprise-me-international" : { + "comment" : "Browser Search query for dinner recipes", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dinner-Rezepte" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "dinner recipes" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "recetas para la cena" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "recettes pour le dîner" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "ricette per la cena" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "recepten voor het avondeten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "przepisy na obiad" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "receitas de jantar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "рецепты на ужин" + } + } + } + }, + "contextual.onboarding.try-search.surprise-me-title" : { + "comment" : "Title for a button that triggers an unknown search query for the user.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Überrasche mich!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Surprise me!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Sorpréndeme!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Surprenez-moi !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sorprendimi!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verras me!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaskocz mnie!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Surpreende-me!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удиви меня!" + } + } + } + }, "copy" : { "comment" : "Copy button", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/Onboarding/ContextualOnboarding/ContextualOnboardingDialogs.swift b/DuckDuckGo/Onboarding/ContextualOnboarding/ContextualOnboardingDialogs.swift new file mode 100644 index 0000000000..63981b9303 --- /dev/null +++ b/DuckDuckGo/Onboarding/ContextualOnboarding/ContextualOnboardingDialogs.swift @@ -0,0 +1,278 @@ +// +// ContextualOnboardingDialogs.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 SwiftUI +import Onboarding +import SwiftUIExtensions + +struct OnboardingDialogsContants { + static let titleFont = Font.system(size: 20, weight: .bold, design: .rounded) + static let messageFont = Font.system(size: Self.messageFontSize, weight: .regular, design: .rounded) + static let messageFontSize = 16.0 +} + +struct OnboardingTrySearchDialog: View { + let title = UserText.ContextualOnboarding.onboardingTryASearchTitle + let message = NSAttributedString(string: UserText.ContextualOnboarding.onboardingTryASearchMessage) + let viewModel: OnboardingSearchSuggestionsViewModel + + var body: some View { + DaxDialogView(logoPosition: .left) { + ContextualDaxDialogContent( + orientation: .horizontalStack(alignment: .top), + title: title, + titleFont: OnboardingDialogsContants.titleFont, + message: message, + messageFont: OnboardingDialogsContants.messageFont, + list: viewModel.itemsList, + listAction: viewModel.listItemPressed + ) + } + .padding() + } + +} + +struct OnboardingTryVisitingSiteDialog: View { + let viewModel: OnboardingSiteSuggestionsViewModel + + var body: some View { + DaxDialogView(logoPosition: .left) { + OnboardingTryVisitingSiteDialogContent(viewModel: viewModel) + } + .padding() + + } +} + +struct OnboardingTryVisitingSiteDialogContent: View { + let message = NSAttributedString(string: UserText.ContextualOnboarding.onboardingTryASiteMessage) + + let viewModel: OnboardingSiteSuggestionsViewModel + + var body: some View { + ContextualDaxDialogContent( + orientation: .horizontalStack(alignment: .top), + title: viewModel.title, + titleFont: OnboardingDialogsContants.titleFont, + message: message, + messageFont: OnboardingDialogsContants.messageFont, + list: viewModel.itemsList, + listAction: viewModel.listItemPressed) + } +} + +struct OnboardingFirstSearchDoneDialog: View { + let title = UserText.ContextualOnboarding.onboardingFirstSearchDoneTitle + let message = NSAttributedString(string: UserText.ContextualOnboarding.onboardingFirstSearchDoneMessage) + let cta = UserText.ContextualOnboarding.onboardingGotItButton + + @State private var showNextScreen: Bool = false + + let shouldFollowUp: Bool + let viewModel: OnboardingSiteSuggestionsViewModel + let gotItAction: () -> Void + + var body: some View { + DaxDialogView(logoPosition: .left) { + VStack { + if showNextScreen { + OnboardingTryVisitingSiteDialogContent(viewModel: viewModel) + } else { + ContextualDaxDialogContent( + orientation: .horizontalStack(alignment: .center), + title: title, + titleFont: OnboardingDialogsContants.titleFont, + message: message, + messageFont: OnboardingDialogsContants.messageFont, + customActionView: AnyView( + OnboardingPrimaryCTAButton(title: cta) { + gotItAction() + withAnimation { + if shouldFollowUp { + showNextScreen = true + } + } + } + ) + ) + } + } + } + .padding() + + } +} + +struct OnboardingFireButtonDialogContent: View { + private let attributedMessage: NSAttributedString = { + let firstString = UserText.ContextualOnboarding.onboardingTryFireButtonMessage + let boldString = "Fire Button." + let attributedString = NSMutableAttributedString(string: firstString) + let boldFontAttribute: [NSAttributedString.Key: Any] = [ + .font: NSFont.systemFont(ofSize: OnboardingDialogsContants.messageFontSize, weight: .bold) + ] + if let boldRange = firstString.range(of: boldString) { + let nsBoldRange = NSRange(boldRange, in: firstString) + attributedString.addAttributes(boldFontAttribute, range: nsBoldRange) + } + + return attributedString + }() + + var body: some View { + ContextualDaxDialogContent( + orientation: .horizontalStack(alignment: .center), + message: attributedMessage, + messageFont: OnboardingDialogsContants.messageFont, + customActionView: AnyView(actionView)) + } + + @ViewBuilder + private var actionView: some View { + VStack { + OnboardingPrimaryCTAButton(title: "Try it", action: {}) + OnboardingSecondaryCTAButton(title: "Skip", action: {}) + } + } + +} + +struct OnboardingFireDialog: View { + + var body: some View { + DaxDialogView(logoPosition: .left) { + VStack { + OnboardingFireButtonDialogContent() + } + } + .padding() + + } +} + +struct OnboardingTrackersDoneDialog: View { + let cta = UserText.ContextualOnboarding.onboardingGotItButton + + @State private var showNextScreen: Bool = false + + let shouldFollowUp: Bool + let message: NSAttributedString + let blockedTrackersCTAAction: () -> Void + + var body: some View { + DaxDialogView(logoPosition: .left) { + VStack { + if showNextScreen { + OnboardingFireButtonDialogContent() + } else { + ContextualDaxDialogContent( + orientation: .horizontalStack(alignment: .center), + message: message, + messageFont: OnboardingDialogsContants.messageFont, + customActionView: AnyView( + OnboardingPrimaryCTAButton(title: cta) { + blockedTrackersCTAAction() + if shouldFollowUp { + withAnimation { + showNextScreen = true + } + } + } + ) + ) + } + } + } + .padding() + + } +} + +struct OnboardingPrimaryCTAButton: View { + let title: String + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(title) + .padding(.vertical, 3) + .padding(.horizontal, 24) + } + .buttonStyle(DefaultActionButtonStyle(enabled: true)) + .shadow(radius: 1, x: -0.6, y: +0.6) + } + +} + +struct OnboardingSecondaryCTAButton: View { + @Environment(\.colorScheme) var colorScheme + + let title: String + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(title) + .padding(.vertical, 3) + .padding(.horizontal, 26) + } + .buttonStyle(DismissActionButtonStyle()) + } + +} + +// MARK: - Preview + +#Preview("Try Search") { + OnboardingTrySearchDialog(viewModel: OnboardingSearchSuggestionsViewModel(suggestedSearchesProvider: OnboardingSuggestedSearchesProvider(), pixelReporter: OnboardingPixelReporter())) + .padding() +} + +final class OnboardingPixelReporter: OnboardingSearchSuggestionsPixelReporting, OnboardingSiteSuggestionsPixelReporting { + func trackSiteSuggetionOptionTapped() { + } + func trackSearchSuggetionOptionTapped() { + } +} + +#Preview("Try Site") { + OnboardingTryVisitingSiteDialog(viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter())) + .padding() +} + +#Preview("First Search Dialog") { + OnboardingFirstSearchDoneDialog(shouldFollowUp: true, viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter()), gotItAction: {}) + .padding() +} + +#Preview("Try Fire Button") { + DaxDialogView(logoPosition: .left) { + OnboardingFireButtonDialogContent() + } + .padding() +} + +#Preview("Trackers Dialog") { + var message: NSAttributedString = { + let firstString = UserText.ContextualOnboarding.onboardingTryFireButtonMessage + return NSMutableAttributedString(string: firstString) + }() + return OnboardingTrackersDoneDialog(shouldFollowUp: true, message: message, blockedTrackersCTAAction: {}) + .padding() +} diff --git a/DuckDuckGo/Onboarding/ContextualOnboarding/OnboardingSuggestedSearchesProvider.swift b/DuckDuckGo/Onboarding/ContextualOnboarding/OnboardingSuggestedSearchesProvider.swift new file mode 100644 index 0000000000..a51454e8de --- /dev/null +++ b/DuckDuckGo/Onboarding/ContextualOnboarding/OnboardingSuggestedSearchesProvider.swift @@ -0,0 +1,79 @@ +// +// OnboardingSuggestedSearchesProvider.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 Onboarding + +struct OnboardingSuggestedSearchesProvider: OnboardingSuggestionsItemsProviding { + private let countryAndLanguageProvider: OnboardingRegionAndLanguageProvider + + init(countryAndLanguageProvider: OnboardingRegionAndLanguageProvider = Locale.current) { + self.countryAndLanguageProvider = countryAndLanguageProvider + } + + var list: [ContextualOnboardingListItem] { + return [ + option1, + option2, + surpriseMe + ] + } + + private var country: String? { + countryAndLanguageProvider.regionCode + } + private var language: String? { + countryAndLanguageProvider.languageCode + } + + private var option1: ContextualOnboardingListItem { + var search: String + if language == "en" { + search = UserText.ContextualOnboarding.tryASearchOption1English + } else { + search = UserText.ContextualOnboarding.tryASearchOption1International + } + return ContextualOnboardingListItem.search(title: search) + } + + private var option2: ContextualOnboardingListItem { + var search: String + if country == "us" { + search = UserText.ContextualOnboarding.tryASearchOption2English + } else { + search = UserText.ContextualOnboarding.tryASearchOption2International + } + return ContextualOnboardingListItem.search(title: search) + } + + private var option3: ContextualOnboardingListItem { + let search = UserText.ContextualOnboarding.tryASearchOption3 + return ContextualOnboardingListItem.search(title: search) + } + + private var surpriseMe: ContextualOnboardingListItem { + var search: String + if country == "us" { + search = UserText.ContextualOnboarding.tryASearchOptionSurpriseMeEnglish + } else { + search = UserText.ContextualOnboarding.tryASearchOptionSurpriseMeInternational + } + return ContextualOnboardingListItem.surprise(title: search, visibleTitle: UserText.ContextualOnboarding.tryASearchOptionSurpriseMeTitle) + } + +} diff --git a/UnitTests/Onboarding/OnboardingSuggestedSearchesProviderTests.swift b/UnitTests/Onboarding/OnboardingSuggestedSearchesProviderTests.swift new file mode 100644 index 0000000000..405f242732 --- /dev/null +++ b/UnitTests/Onboarding/OnboardingSuggestedSearchesProviderTests.swift @@ -0,0 +1,77 @@ +// +// OnboardingSuggestedSearchesProviderTests.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 XCTest +import Onboarding +@testable import DuckDuckGo_Privacy_Browser + +class OnboardingSuggestedSearchesProviderTests: XCTestCase { + + let userText = UserText.ContextualOnboarding.self + + func testSearchesListForEnglishLanguageAndUsRegion() { + let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "us", languageCode: "en") + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider) + + let expectedSearches = [ + ContextualOnboardingListItem.search(title: userText.tryASearchOption1English), + ContextualOnboardingListItem.search(title: userText.tryASearchOption2English), + ContextualOnboardingListItem.surprise(title: userText.tryASearchOptionSurpriseMeEnglish, visibleTitle: UserText.ContextualOnboarding.tryASearchOptionSurpriseMeTitle) + ] + + XCTAssertEqual(provider.list, expectedSearches) + } + + func testSearchesListForNonEnglishLanguageAndNonUSRegion() { + let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "fr", languageCode: "fr") + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider) + + let expectedSearches = [ + ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), + ContextualOnboardingListItem.search(title: userText.tryASearchOption2International), + ContextualOnboardingListItem.surprise(title: userText.tryASearchOptionSurpriseMeInternational, visibleTitle: UserText.ContextualOnboarding.tryASearchOptionSurpriseMeTitle) + ] + + XCTAssertEqual(provider.list, expectedSearches) + } + + func testSearchesListForUSRegionAndNonEnglishLanguage() { + let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "us", languageCode: "es") + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider) + + let expectedSearches = [ + ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), + ContextualOnboardingListItem.search(title: userText.tryASearchOption2English), + ContextualOnboardingListItem.surprise(title: userText.tryASearchOptionSurpriseMeEnglish, visibleTitle: UserText.ContextualOnboarding.tryASearchOptionSurpriseMeTitle) + ] + + XCTAssertEqual(provider.list, expectedSearches) + } +} + +class MockOnboardingRegionAndLanguageProvider: OnboardingRegionAndLanguageProvider { + var regionCode: String? + var languageCode: String? + + init(regionCode: String?, languageCode: String?) { + self.regionCode = regionCode + self.languageCode = languageCode + } +}