Bug 1664510 - Add localization for the WDBA r=flod,nalexander
authorKirk Steuber <ksteuber@mozilla.com>
Fri, 18 Sep 2020 20:27:40 +0000
changeset 549479 d6c9e691db15a4ed7388d0338f29b8af8907b32d
parent 549478 a3477f5f773125f0b92f3d488dd2bd96390163da
child 549480 a5cdfde00f159276453541049a8cbb5f7043ae2a
child 549481 97e77b84546f6f22d8f9c54208c186d8b1a96f38
push id37800
push userdluca@mozilla.com
push dateMon, 21 Sep 2020 21:36:12 +0000
treeherdermozilla-central@a5cdfde00f15 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflod, nalexander
bugs1664510
milestone82.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1664510 - Add localization for the WDBA r=flod,nalexander This patch adds localization to the WDBA in a somewhat unusual manner. It's very difficult to localize toast notifications because the button text tends to overflow, which the localizers won't realize. Therefore, we are making a simpler version of the toast notification that only uses "Yes" and "No" as the button text. In English, we will not use this version and will continue using the version we have been using. This means that the localized toast notifications will not have an "Ask me later" button, so the behavior will differ a bit from the English version. Specifically, there is no "followup" for the localized notification since there is no button that ought to cause it to be shown. We've considered and rejected using localizer comments to keep these button strings short. We are limited by the amount of space the text takes up, not the number of characters. We just don't have a good way to convey this information to localizers in an actionable way. Differential Revision: https://phabricator.services.mozilla.com/D90608
browser/locales/en-US/defaultagent/defaultagent_localized.ini
toolkit/mozapps/defaultagent/Notification.cpp
toolkit/mozapps/defaultagent/defaultagent_append.ini
--- a/browser/locales/en-US/defaultagent/defaultagent_localized.ini
+++ b/browser/locales/en-US/defaultagent/defaultagent_localized.ini
@@ -1,7 +1,12 @@
 ; This Source Code Form is subject to the terms of the Mozilla Public
 ; License, v. 2.0. If a copy of the MPL was not distributed with this
 ; file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 ; This file is in the UTF-8 encoding
 [Strings]
 DefaultBrowserAgentTaskDescription=The Default Browser Agent task checks when the default changes from %MOZ_APP_DISPLAYNAME% to another browser. If the change happens under suspicious circumstances, it will prompt users to change back to %MOZ_APP_DISPLAYNAME% no more than two times. This task is installed automatically by %MOZ_APP_DISPLAYNAME%, and is reinstalled when %MOZ_APP_DISPLAYNAME% updates. To disable this task, update the “default-browser-agent.enabled” preference on the about:config page or the %MOZ_APP_DISPLAYNAME% enterprise policy setting “DisableDefaultBrowserAgent”.
+
+DefaultBrowserNotificationTitle=Make %MOZ_APP_DISPLAYNAME% your default browser
+DefaultBrowserNotificationText=%MOZ_APP_DISPLAYNAME% is no longer your default browser. Make it your default?
+DefaultBrowserNotificationYesButtonText=Yes
+DefaultBrowserNotificationNoButtonText=No
--- a/toolkit/mozapps/defaultagent/Notification.cpp
+++ b/toolkit/mozapps/defaultagent/Notification.cpp
@@ -29,16 +29,18 @@
 
 // If the notification hasn't been activated or dismissed within 30 minutes,
 // stop waiting for it.
 #define NOTIFICATION_WAIT_TIMEOUT_MS (30 * 60 * 1000)
 // If the mutex hasn't been released within a few minutes, something is wrong
 // and we should give up on it
 #define MUTEX_TIMEOUT_MS (10 * 60 * 1000)
 
+bool FirefoxInstallIsEnglish();
+
 static bool SetInitialNotificationShown(bool wasShown) {
   return !RegistrySetValueBool(IsPrefixed::Unprefixed,
                                L"InitialNotificationShown", wasShown)
               .isErr();
 }
 
 static bool GetInitialNotificationShown() {
   return RegistryGetValueBool(IsPrefixed::Unprefixed,
@@ -92,21 +94,38 @@ struct ToastStrings {
   mozilla::UniquePtr<wchar_t[]> text1;
   mozilla::UniquePtr<wchar_t[]> text2;
   mozilla::UniquePtr<wchar_t[]> action1;
   mozilla::UniquePtr<wchar_t[]> action2;
   mozilla::UniquePtr<wchar_t[]> relImagePath;
 };
 
 struct Strings {
+  // Toast notification button text is hard to localize because it tends to
+  // overflow. Thus, we have 3 different toast notifications:
+  //  - The initial notification, which includes a button with text like
+  //    "Ask me later". Since we cannot easily localize this, we will display
+  //    it only in English.
+  //  - The followup notification, to be shown if the user clicked "Ask me
+  //    later". Since we only have that button in English, we only need this
+  //    notification in English.
+  //  - The localized notification, which has much shorter button text to
+  //    (hopefully) prevent overflow: just "Yes" and "No". Since we no longer
+  //    have an "Ask me later" button, a followup localized notification is not
+  //    needed.
   ToastStrings initialToast;
   ToastStrings followupToast;
+  ToastStrings localizedToast;
 
   // Returned pointer points within this struct and should not be freed.
-  const ToastStrings* GetToastStrings(NotificationType whichToast) const {
+  const ToastStrings* GetToastStrings(NotificationType whichToast,
+                                      bool englishStrings) const {
+    if (!englishStrings) {
+      return &localizedToast;
+    }
     if (whichToast == NotificationType::Initial) {
       return &initialToast;
     }
     return &followupToast;
   }
 };
 
 // Gets a string out of the specified INI file.
@@ -121,20 +140,17 @@ static bool GetString(const wchar_t* ini
     LOG_ERROR_MESSAGE(
         L"Unable to retrieve INI string: section=%S, key=%S, result=%d",
         section, key, result);
     return false;
   }
   return true;
 }
 
-// Gets all strings out of the localized INI file.
-// Note that, at this time, the INI is not actually being localized - it only
-// exists in English. This is why MaybeShowNotification() checks IsEnglish().
-// See Bug 1621696 for more information.
+// Gets all strings out of the relevant INI files.
 // Returns true on success, false on failure
 static bool GetStrings(Strings& strings) {
   mozilla::UniquePtr<wchar_t[]> installPath;
   bool success = GetInstallDirectory(installPath);
   if (!success) {
     LOG_ERROR_MESSAGE(L"Failed to get install directory when getting strings");
     return false;
   }
@@ -160,27 +176,52 @@ static bool GetStrings(Strings& strings)
   stringsReader.AddKey("DefaultBrowserNotificationDontShowAgain",
                        &strings.followupToast.action1);
   stringsReader.AddKey("DefaultBrowserNotificationMakeFirefoxDefault",
                        &strings.initialToast.action2);
   stringsReader.AddKey("DefaultBrowserNotificationMakeFirefoxDefault",
                        &strings.followupToast.action2);
   int result = stringsReader.Read();
   if (result != OK) {
+    LOG_ERROR_MESSAGE(L"Unable to read English strings: %d", result);
+    return false;
+  }
+
+  const wchar_t* localizedIniFormat = L"%s\\defaultagent_localized.ini";
+  bufferSize = _scwprintf(localizedIniFormat, installPath.get());
+  ++bufferSize;  // Extra character for terminating null
+  mozilla::UniquePtr<wchar_t[]> localizedIniPath =
+      mozilla::MakeUnique<wchar_t[]>(bufferSize);
+  _snwprintf_s(localizedIniPath.get(), bufferSize, _TRUNCATE,
+               localizedIniFormat, installPath.get());
+
+  IniReader localizedReader(localizedIniPath.get());
+  localizedReader.AddKey("DefaultBrowserNotificationTitle",
+                         &strings.localizedToast.text1);
+  localizedReader.AddKey("DefaultBrowserNotificationText",
+                         &strings.localizedToast.text2);
+  localizedReader.AddKey("DefaultBrowserNotificationYesButtonText",
+                         &strings.localizedToast.action1);
+  localizedReader.AddKey("DefaultBrowserNotificationNoButtonText",
+                         &strings.localizedToast.action2);
+  result = localizedReader.Read();
+  if (result != OK) {
     LOG_ERROR_MESSAGE(L"Unable to read localized strings: %d", result);
     return false;
   }
 
   // IniReader is only capable of reading from one section at a time, so we need
   // to make another one to read the other section.
   IniReader nonlocalizedReader(iniPath.get(), "Nonlocalized");
   nonlocalizedReader.AddKey("InitialToastRelativeImagePath",
                             &strings.initialToast.relImagePath);
   nonlocalizedReader.AddKey("FollowupToastRelativeImagePath",
                             &strings.followupToast.relImagePath);
+  nonlocalizedReader.AddKey("LocalizedToastRelativeImagePath",
+                            &strings.localizedToast.relImagePath);
   result = nonlocalizedReader.Read();
   if (result != OK) {
     LOG_ERROR_MESSAGE(L"Unable to read non-localized strings: %d", result);
     return false;
   }
 
   return true;
 }
@@ -206,21 +247,24 @@ struct HandlerData {
 // we could deconstruct the mutex while the handler is holding it and is
 // already accessing the protected data.
 static HandlerData gHandlerReturnData;
 static HANDLE gHandlerMutex = INVALID_HANDLE_VALUE;
 
 class ToastHandler : public WinToastLib::IWinToastHandler {
  private:
   NotificationType mWhichNotification;
+  bool mIsLocalizedNotification;
   HANDLE mEvent;
 
  public:
-  ToastHandler(NotificationType whichNotification, HANDLE event) {
+  ToastHandler(NotificationType whichNotification, bool isEnglishInstall,
+               HANDLE event) {
     mWhichNotification = whichNotification;
+    mIsLocalizedNotification = !isEnglishInstall;
     mEvent = event;
   }
 
   void FinishHandler(NotificationActivities& returnData) const {
     SetReturnData(returnData);
 
     BOOL success = SetEvent(mEvent);
     if (!success) {
@@ -268,32 +312,41 @@ class ToastHandler : public WinToastLib:
 
   void toastActivated(int actionIndex) const override {
     NotificationActivities activitiesPerformed;
     activitiesPerformed.type = mWhichNotification;
     activitiesPerformed.shown = NotificationShown::Shown;
     // Override this below
     activitiesPerformed.action = NotificationAction::NoAction;
 
-    if (actionIndex == 0) {
-      if (mWhichNotification == NotificationType::Initial) {
+    // The if conditionals here are a little confusing to read because on the
+    // initial and followup notifications, the "Make Firefox the default" button
+    // is on the right, but on the localized notification, the equivalent button
+    // ("Yes") is on the left side.
+    if ((actionIndex == 0 && !mIsLocalizedNotification) ||
+        (actionIndex == 1 && mIsLocalizedNotification)) {
+      if (mWhichNotification == NotificationType::Initial &&
+          !mIsLocalizedNotification) {
         // "Remind me later" button
         activitiesPerformed.action = NotificationAction::RemindMeLater;
         if (!SetFollowupNotificationRequestTime(GetCurrentTimestamp())) {
           LOG_ERROR_MESSAGE(L"Unable to schedule followup notification");
         }
       } else {
-        // "Don't ask again" button
+        // "Don't ask again" button on the followup notification, or "No" on the
+        // localized notification.
         // Do nothing. As long as we don't call
         // SetFollowupNotificationRequestTime, there will be no followup
         // notification.
         activitiesPerformed.action = NotificationAction::DismissedByButton;
       }
-    } else if (actionIndex == 1) {
-      // "Make Firefox the default" button, on both notifications.
+    } else if ((actionIndex == 1 && !mIsLocalizedNotification) ||
+               (actionIndex == 0 && mIsLocalizedNotification)) {
+      // "Make Firefox the default" button, on both the initial and followup
+      // notifications. "Yes" button on the localized notification.
       activitiesPerformed.action = NotificationAction::MakeFirefoxDefaultButton;
       LaunchModernSettingsDialogDefaultApps();
     }
 
     FinishHandler(activitiesPerformed);
   }
 
   void toastDismissed(WinToastDismissalReason state) const override {
@@ -345,21 +398,24 @@ static NotificationActivities ShowNotifi
   std::wstring aumiStr = aumi;
   WinToast::instance()->setAppUserModelId(aumiStr);
   WinToast::WinToastError error;
   if (!WinToast::instance()->initialize(&error)) {
     LOG_ERROR_MESSAGE(WinToast::strerror(error).c_str());
     return activitiesPerformed;
   }
 
+  bool isEnglishInstall = FirefoxInstallIsEnglish();
+
   Strings strings;
   if (!GetStrings(strings)) {
     return activitiesPerformed;
   }
-  const ToastStrings* toastStrings = strings.GetToastStrings(whichNotification);
+  const ToastStrings* toastStrings =
+      strings.GetToastStrings(whichNotification, isEnglishInstall);
 
   // This event object will let the handler notify us when it has handled the
   // notification.
   nsAutoHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr));
   if (event.get() == nullptr) {
     LOG_ERROR_MESSAGE(L"Unable to create event object: %#X", GetLastError());
     return activitiesPerformed;
   }
@@ -417,17 +473,18 @@ static NotificationActivities ShowNotifi
       WinToastTemplate(WinToastTemplate::ImageAndText02);
   toastTemplate.setTextField(toastStrings->text1.get(),
                              WinToastTemplate::FirstLine);
   toastTemplate.setTextField(toastStrings->text2.get(),
                              WinToastTemplate::SecondLine);
   toastTemplate.addAction(toastStrings->action1.get());
   toastTemplate.addAction(toastStrings->action2.get());
   toastTemplate.setImagePath(absImagePath.get());
-  ToastHandler* handler = new ToastHandler(whichNotification, event.get());
+  ToastHandler* handler =
+      new ToastHandler(whichNotification, isEnglishInstall, event.get());
   INT64 id = WinToast::instance()->showToast(toastTemplate, handler, &error);
   if (id < 0) {
     LOG_ERROR_MESSAGE(WinToast::strerror(error).c_str());
     return activitiesPerformed;
   }
 
   DWORD result = WaitForSingleObject(event.get(), NOTIFICATION_WAIT_TIMEOUT_MS);
   // Don't return after these errors. Attempt to hide the notification.
@@ -464,29 +521,20 @@ static NotificationActivities ShowNotifi
   }
 
   if (!WinToast::instance()->hideToast(id)) {
     LOG_ERROR_MESSAGE(L"Failed to hide notification");
   }
   return activitiesPerformed;
 }
 
-// This function checks that both the Firefox build and the operating system
-// are using English. This is checked because this feature is not yet being
-// localized.
-bool IsEnglish() {
-  mozilla::UniquePtr<wchar_t[]> windowsLocale =
-      mozilla::MakeUnique<wchar_t[]>(LOCALE_NAME_MAX_LENGTH);
-  int result =
-      GetUserDefaultLocaleName(windowsLocale.get(), LOCALE_NAME_MAX_LENGTH);
-  if (result == 0) {
-    LOG_ERROR_MESSAGE(L"Unable to get locale: %#X", GetLastError());
-    return false;
-  }
-
+// This function checks that the Firefox build is using English. This is checked
+// because of the peculiar way we are localizing toast notifications where we
+// use a completely different set of strings in English.
+bool FirefoxInstallIsEnglish() {
   mozilla::UniquePtr<wchar_t[]> installPath;
   bool success = GetInstallDirectory(installPath);
   if (!success) {
     LOG_ERROR_MESSAGE(L"Failed to get install directory when getting strings");
     return false;
   }
   const wchar_t* iniFormat = L"%s\\locale.ini";
   int bufferSize = _scwprintf(iniFormat, installPath.get());
@@ -496,36 +544,33 @@ bool IsEnglish() {
   _snwprintf_s(iniPath.get(), bufferSize, _TRUNCATE, iniFormat,
                installPath.get());
 
   mozilla::UniquePtr<wchar_t[]> firefoxLocale;
   if (!GetString(iniPath.get(), "locale", "locale", firefoxLocale)) {
     return false;
   }
 
-  return _wcsnicmp(windowsLocale.get(), L"en-", 3) == 0 &&
-         _wcsnicmp(firefoxLocale.get(), L"en-", 3) == 0;
+  return _wcsnicmp(firefoxLocale.get(), L"en-", 3) == 0;
 }
 
 // If a notification is shown, this function will block until the notification
 // is activated or dismissed.
 // aumi is the App User Model ID.
 NotificationActivities MaybeShowNotification(
     const DefaultBrowserInfo& browserInfo, const wchar_t* aumi) {
   // Default to not showing a notification. Any other value will be returned
   // directly from ShowNotification.
   NotificationActivities activitiesPerformed = {NotificationType::Initial,
                                                 NotificationShown::NotShown,
                                                 NotificationAction::NoAction};
 
-  if (!mozilla::IsWin10OrLater() || !IsEnglish()) {
+  if (!mozilla::IsWin10OrLater()) {
     // Notifications aren't shown in versions prior to Windows 10 because the
     // notification API we want isn't available.
-    // They are also not shown in non-English contexts because localization is
-    // not yet being done for this component.
     return activitiesPerformed;
   }
 
   bool initialNotificationShown = GetInitialNotificationShown();
   if (!initialNotificationShown) {
     if (browserInfo.currentDefaultBrowser == Browser::EdgeWithBlink &&
         browserInfo.previousDefaultBrowser == Browser::Firefox) {
       return ShowNotification(NotificationType::Initial, aumi);
--- a/toolkit/mozapps/defaultagent/defaultagent_append.ini
+++ b/toolkit/mozapps/defaultagent/defaultagent_append.ini
@@ -1,7 +1,8 @@
 
 ; IMPORTANT: This file should always start with a newline in case a locale
 ;            provided INI does not end with a newline.
 
 [Nonlocalized]
 InitialToastRelativeImagePath=browser/VisualElements/VisualElements_150.png
 FollowupToastRelativeImagePath=browser/VisualElements/VisualElements_150.png
+LocalizedToastRelativeImagePath=browser/VisualElements/VisualElements_150.png