Bug 1755519 - Add language switching to about:welcome; r=pdahiya,platform-i18n-reviewers,flod,dminor

This patch ended up adding some complexity to about:welcome, as the
language switching needs to eagerly perform fallible asynchronous
actions. Specifically it needs to get the list of addons and
pre-emptively install the langpack, which can take time, and can fail.

This necessitated building a custom React components and custom hooks to
be able to deal with these requirements.

The following command will allow for the testing of this feature.

./mach run \
  --temp-profile \
  --setpref "extensions.getAddons.langpacks.url=https://mock-amo-language-tools.glitch.me/?app=firefox&type=language&appversion=%VERSION%" \
  --setpref "intl.multilingual.aboutWelcome.languageMismatchEnabled=true" \
  --setpref "intl.multilingual.aboutWelcome.systemLocaleOverride=es-ES" `#(optional)` \
  -- --new-tab about:welcome

Differential Revision: https://phabricator.services.mozilla.com/D138831
This commit is contained in:
Greg Tatum 2022-03-02 15:52:43 +00:00
Родитель 669f31b897
Коммит 7890042033
17 изменённых файлов: 1388 добавлений и 29 удалений

Просмотреть файл

@ -2174,6 +2174,8 @@ pref("app.normandy.onsync_skew_sec", 600);
// live reloading when switching between LTR and RTL languages.
pref("intl.multilingual.liveReload", false);
pref("intl.multilingual.liveReloadBidirectional", false);
// Suggest to change the language on about:welcome when there is a mismatch with the OS.
pref("intl.multilingual.aboutWelcome.languageMismatchEnabled", false);
// Simulate conditions that will happen when the browser

Просмотреть файл

@ -162,6 +162,22 @@ class AboutWelcomeChild extends JSWindowActorChild {
Cu.exportFunction(this.AWFinish.bind(this), window, {
defineAs: "AWFinish",
});
Cu.exportFunction(this.AWEnsureLangPackInstalled.bind(this), window, {
defineAs: "AWEnsureLangPackInstalled",
});
Cu.exportFunction(
this.AWNegotiateLangPackForLanguageMismatch.bind(this),
window,
{
defineAs: "AWNegotiateLangPackForLanguageMismatch",
}
);
Cu.exportFunction(this.AWSetRequestedLocales.bind(this), window, {
defineAs: "AWSetRequestedLocales",
});
}
/**
@ -173,6 +189,20 @@ class AboutWelcomeChild extends JSWindowActorChild {
);
}
/**
* Clones the result of the query into the content window.
*/
sendQueryAndCloneForContent(...sendQueryArgs) {
return this.wrapPromise(
(async () => {
return Cu.cloneInto(
await this.sendQuery(...sendQueryArgs),
this.contentWindow
);
})()
);
}
AWSelectTheme(data) {
return this.wrapPromise(
this.sendQuery("AWPage:SELECT_THEME", data.toUpperCase())
@ -206,6 +236,11 @@ class AboutWelcomeChild extends JSWindowActorChild {
let featureConfig = NimbusFeatures.aboutwelcome.getAllVariables();
featureConfig.needDefault = await this.sendQuery("AWPage:NEED_DEFAULT");
featureConfig.needPin = await this.sendQuery("AWPage:DOES_APP_NEED_PIN");
if (featureConfig.languageMismatchEnabled) {
featureConfig.appAndSystemLocaleInfo = await this.sendQuery(
"AWPage:GET_APP_AND_SYSTEM_LOCALE_INFO"
);
}
let defaults = AboutWelcomeDefaults.getDefaults();
// FeatureConfig (from prefs or experiments) has higher precendence
// to defaults. But the `screens` property isn't defined we shouldn't
@ -277,6 +312,27 @@ class AboutWelcomeChild extends JSWindowActorChild {
this.contentWindow.location.href = "about:home";
}
AWEnsureLangPackInstalled(langPack) {
return this.sendQueryAndCloneForContent(
"AWPage:ENSURE_LANG_PACK_INSTALLED",
langPack
);
}
AWSetRequestedLocales(requestSystemLocales) {
return this.sendQueryAndCloneForContent(
"AWPage:SET_REQUESTED_LOCALES",
requestSystemLocales
);
}
AWNegotiateLangPackForLanguageMismatch(appAndSystemLocaleInfo) {
return this.sendQueryAndCloneForContent(
"AWPage:NEGOTIATE_LANGPACK",
appAndSystemLocaleInfo
);
}
/**
* @param {{type: string, detail?: any}} event
* @override

Просмотреть файл

@ -25,6 +25,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
Region: "resource://gre/modules/Region.jsm",
ShellService: "resource:///modules/ShellService.jsm",
LangPackMatcher: "resource://gre/modules/LangPackMatcher.jsm",
});
XPCOMUtils.defineLazyGetter(this, "log", () => {
@ -304,6 +305,14 @@ class AboutWelcomeParent extends JSWindowActorParent {
}
})
);
case "AWPage:GET_APP_AND_SYSTEM_LOCALE_INFO":
return LangPackMatcher.getAppAndSystemLocaleInfo();
case "AWPage:NEGOTIATE_LANGPACK":
return LangPackMatcher.negotiateLangPackForLanguageMismatch(data);
case "AWPage:ENSURE_LANG_PACK_INSTALLED":
return LangPackMatcher.ensureLangPackInstalled(data);
case "AWPage:SET_REQUESTED_LOCALES":
return LangPackMatcher.setRequestedAppLocales(data);
default:
log.debug(`Unexpected event ${type} was not handled.`);
}

Просмотреть файл

@ -101,7 +101,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var _components_MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3);
/* harmony import */ var _components_ReturnToAMO__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(10);
/* harmony import */ var _components_ReturnToAMO__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(11);
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
/* This Source Code Form is subject to the terms of the Mozilla Public
@ -191,7 +191,8 @@ class AboutWelcome extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComp
metricsFlowUri: this.state.metricsFlowUri,
utm_term: props.UTMTerm,
transitions: props.transitions,
backdrop: props.backdrop
backdrop: props.backdrop,
appAndSystemLocaleInfo: props.appAndSystemLocaleInfo
});
}
@ -276,7 +277,8 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var _MSLocalized__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(5);
/* harmony import */ var _MultiStageProtonScreen__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(6);
/* harmony import */ var _asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(9);
/* harmony import */ var _LanguageSwitcher__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(9);
/* harmony import */ var _asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(10);
/* 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/. */
@ -284,14 +286,18 @@ __webpack_require__.r(__webpack_exports__);
// Amount of milliseconds for all transitions to complete (including delays).
const TRANSITION_OUT_TIME = 1000;
const MultiStageAboutWelcome = props => {
let {
screens
} = props;
const [index, setScreenIndex] = Object(react__WEBPACK_IMPORTED_MODULE_0__["useState"])(0);
Object(react__WEBPACK_IMPORTED_MODULE_0__["useEffect"])(() => {
// Send impression ping when respective screen first renders
props.screens.forEach((screen, order) => {
screens.forEach((screen, order) => {
if (index === order) {
_lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__["AboutWelcomeUtils"].sendImpressionTelemetry(`${props.message_id}_${order}_${screen.id}`);
}
@ -307,7 +313,7 @@ const MultiStageAboutWelcome = props => {
// button from about:home
const handler = ({
state
}) => setScreenIndex(Math.min(state, props.screens.length - 1)); // Handle page load, e.g., going back to about:welcome from about:home
}) => setScreenIndex(Math.min(state, screens.length - 1)); // Handle page load, e.g., going back to about:welcome from about:home
handler(window.history); // Watch for browser back/forward button navigation events
@ -345,7 +351,7 @@ const MultiStageAboutWelcome = props => {
setTransition(props.transitions ? "out" : ""); // Actually move forwards after all transitions finish.
setTimeout(() => {
if (index < props.screens.length - 1) {
if (index < screens.length - 1) {
setTransition(props.transitions ? "in" : "");
setScreenIndex(prevState => prevState + 1);
} else {
@ -400,18 +406,24 @@ const MultiStageAboutWelcome = props => {
})();
}, [useImportable, region]);
const centeredScreens = props.screens.filter(s => s.content.position !== "corner");
const {
negotiatedLanguage,
langPackInstallPhase,
languageFilteredScreens
} = Object(_LanguageSwitcher__WEBPACK_IMPORTED_MODULE_4__["useLanguageSwitcher"])(props.appAndSystemLocaleInfo, screens, index, setScreenIndex);
screens = languageFilteredScreens;
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_0___default.a.Fragment, null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
className: `outer-wrapper onboardingContainer proton transition-${transition}`,
style: props.backdrop ? {
background: props.backdrop
} : {}
}, props.screens.map((screen, order) => {
}, screens.map((screen, order) => {
const isFirstCenteredScreen = screen.content.position !== "corner" && screen.order === centeredScreens[0].order;
const isLastCenteredScreen = screen.content.position !== "corner" && screen.order === centeredScreens[centeredScreens.length - 1].order;
return index === order ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(WelcomeScreen, {
key: screen.id + order,
id: screen.id,
totalNumberOfScreens: props.screens.length,
totalNumberOfScreens: screens.length,
isFirstCenteredScreen: isFirstCenteredScreen,
isLastCenteredScreen: isLastCenteredScreen,
order: order,
@ -424,7 +436,9 @@ const MultiStageAboutWelcome = props => {
activeTheme: activeTheme,
initialTheme: initialTheme,
setActiveTheme: setActiveTheme,
autoAdvance: screen.auto_advance
autoAdvance: screen.auto_advance,
negotiatedLanguage: negotiatedLanguage,
langPackInstallPhase: langPackInstallPhase
}) : null;
})));
};
@ -468,7 +482,7 @@ class WelcomeScreen extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureCom
} = action;
if (type === "SHOW_FIREFOX_ACCOUNTS") {
let params = { ..._asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_4__["BASE_PARAMS"],
let params = { ..._asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_5__["BASE_PARAMS"],
utm_term: `aboutwelcome-${UTMTerm}-screen`
};
@ -483,7 +497,7 @@ class WelcomeScreen extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureCom
};
} else if (type === "OPEN_URL") {
let url = new URL(data.args);
Object(_asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_4__["addUtmParams"])(url, `aboutwelcome-${UTMTerm}-screen`);
Object(_asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_5__["addUtmParams"])(url, `aboutwelcome-${UTMTerm}-screen`);
if (action.addFlowParams && flowParams) {
url.searchParams.append("device_id", flowParams.deviceId);
@ -509,7 +523,7 @@ class WelcomeScreen extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureCom
let {
value
} = event.currentTarget;
let targetContent = props.content[value] || props.content.tiles;
let targetContent = props.content[value] || props.content.tiles || props.content.languageSwitcher;
if (!(targetContent && targetContent.action)) {
return;
@ -551,7 +565,11 @@ class WelcomeScreen extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureCom
order: this.props.order,
activeTheme: this.props.activeTheme,
totalNumberOfScreens: this.props.totalNumberOfScreens - 1,
appAndSystemLocaleInfo: this.props.appAndSystemLocaleInfo,
negotiatedLanguage: this.props.negotiatedLanguage,
langPackInstallPhase: this.props.langPackInstallPhase,
handleAction: this.handleAction,
messageId: this.props.messageId,
isFirstCenteredScreen: this.props.isFirstCenteredScreen,
isLastCenteredScreen: this.props.isLastCenteredScreen,
autoAdvance: this.props.autoAdvance
@ -772,6 +790,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var _Colorways__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(7);
/* harmony import */ var _Themes__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8);
/* harmony import */ var _MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(3);
/* harmony import */ var _LanguageSwitcher__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(9);
/* 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/. */
@ -780,6 +799,7 @@ __webpack_require__.r(__webpack_exports__);
const MultiStageProtonScreen = props => {
const {
autoAdvance,
@ -812,7 +832,10 @@ const MultiStageProtonScreen = props => {
autoAdvance: props.autoAdvance,
isRtamo: props.isRtamo,
isTheme: props.isTheme,
iconURL: props.iconURL
iconURL: props.iconURL,
messageId: props.messageId,
negotiatedLanguage: props.negotiatedLanguage,
langPackInstallPhase: props.langPackInstallPhase
});
};
class ProtonScreen extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComponent {
@ -869,7 +892,19 @@ class ProtonScreen extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComp
}) : null);
}
renderLanguageSwitcher() {
return this.props.content.languageSwitcher ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_LanguageSwitcher__WEBPACK_IMPORTED_MODULE_5__["LanguageSwitcher"], {
content: this.props.content,
handleAction: this.props.handleAction,
negotiatedLanguage: this.props.negotiatedLanguage,
langPackInstallPhase: this.props.langPackInstallPhase,
messageId: this.props.messageId
}) : null;
}
render() {
var _this$props$appAndSys, _content$primary_butt;
const {
autoAdvance,
content,
@ -940,13 +975,15 @@ class ProtonScreen extends react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComp
text: content.subtitle
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h2", {
"data-l10n-args": JSON.stringify({
"addon-name": this.props.addonName
"addon-name": this.props.addonName,
...((_this$props$appAndSys = this.props.appAndSystemLocaleInfo) === null || _this$props$appAndSys === void 0 ? void 0 : _this$props$appAndSys.displayNames)
})
})) : null), this.renderContentTiles(), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
})) : null), this.renderContentTiles(), this.renderLanguageSwitcher(), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
text: content.primary_button ? content.primary_button.label : null
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", {
className: "primary",
value: "primary_button",
disabled: ((_content$primary_butt = content.primary_button) === null || _content$primary_butt === void 0 ? void 0 : _content$primary_butt.disabled) === true,
onClick: this.props.handleAction
})), content.secondary_button ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_4__["SecondaryCTA"], {
content: content,
@ -1214,6 +1251,253 @@ const Themes = props => {
/* 9 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "useLanguageSwitcher", function() { return useLanguageSwitcher; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "LanguageSwitcher", function() { return LanguageSwitcher; });
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _MSLocalized__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(5);
/* 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/. */
/**
* The language switcher implements a hook that should be placed at a higher level
* than the actual language switcher component, as it needs to preemptively fetch
* and install langpacks for the user if there is a language mismatch screen.
*/
function useLanguageSwitcher(appAndSystemLocaleInfo, screens, screenIndex, setScreenIndex) {
const languageMismatchScreenIndex = screens.findIndex(({
id
}) => id === "AW_LANGUAGE_MISMATCH");
const screen = screens[languageMismatchScreenIndex]; // If there is a mismatch, then Firefox can negotiate a better langpack to offer
// the user.
const [negotiatedLanguage, setNegotiatedLanguage] = Object(react__WEBPACK_IMPORTED_MODULE_0__["useState"])(null);
Object(react__WEBPACK_IMPORTED_MODULE_0__["useEffect"])(function getNegotiatedLanguage() {
if (!appAndSystemLocaleInfo) {
return;
}
if (appAndSystemLocaleInfo.matchType !== "language-mismatch") {
// There is no language mismatch, so there is no need to negotiate a langpack.
return;
}
(async () => {
const langPack = await window.AWNegotiateLangPackForLanguageMismatch(appAndSystemLocaleInfo);
if (langPack) {
// Convert the BCP 47 identifiers into the proper display names.
// e.g. "fr-CA" -> "Canadian French".
const displayNames = new Intl.DisplayNames(appAndSystemLocaleInfo.appLocaleRaw, {
type: "language"
});
setNegotiatedLanguage({
displayName: displayNames.of(langPack.target_locale),
langPack,
requestSystemLocales: [langPack.target_locale, appAndSystemLocaleInfo.appLocaleRaw]
});
} else {
setNegotiatedLanguage({
displayName: null,
langPack: null,
requestSystemLocales: null
});
}
})();
}, [appAndSystemLocaleInfo]);
/**
* @type {
* "before-installation"
* | "installing"
* | "installed"
* | "installation-error"
* | "none-available"
* }
*/
const [langPackInstallPhase, setLangPackInstallPhase] = Object(react__WEBPACK_IMPORTED_MODULE_0__["useState"])("before-installation");
Object(react__WEBPACK_IMPORTED_MODULE_0__["useEffect"])(function ensureLangPackInstalled() {
if (!negotiatedLanguage) {
// There are no negotiated languages to download yet.
return;
}
setLangPackInstallPhase("installing");
window.AWEnsureLangPackInstalled(negotiatedLanguage.langPack).then(() => {
setLangPackInstallPhase("installed");
}, error => {
console.error(error);
setLangPackInstallPhase("installation-error");
});
}, [negotiatedLanguage]);
const shouldHideLanguageSwitcher = screen && (appAndSystemLocaleInfo === null || appAndSystemLocaleInfo === void 0 ? void 0 : appAndSystemLocaleInfo.matchType) !== "language-mismatch";
const [languageFilteredScreens, setLanguageFilteredScreens] = Object(react__WEBPACK_IMPORTED_MODULE_0__["useState"])(screens);
Object(react__WEBPACK_IMPORTED_MODULE_0__["useEffect"])(function filterScreen() {
if (shouldHideLanguageSwitcher || (negotiatedLanguage === null || negotiatedLanguage === void 0 ? void 0 : negotiatedLanguage.langPack) === null) {
if (screenIndex > languageMismatchScreenIndex) {
setScreenIndex(screenIndex - 1);
}
setLanguageFilteredScreens(screens.filter(s => s.id !== "AW_LANGUAGE_MISMATCH"));
} else {
setLanguageFilteredScreens(screens);
}
}, [screens, negotiatedLanguage]);
return {
negotiatedLanguage,
langPackInstallPhase,
languageFilteredScreens
};
}
/**
* The language switcher is a separate component as it needs to perform some asynchronous
* network actions such as retrieving the list of langpacks available, and downloading
* a new langpack. On a fast connection, this won't be noticeable, but on slow or unreliable
* internet this may fail for a user.
*/
function LanguageSwitcher(props) {
const {
content,
handleAction,
negotiatedLanguage,
langPackInstallPhase,
messageId
} = props;
const [isAwaitingLangpack, setIsAwaitingLangpack] = Object(react__WEBPACK_IMPORTED_MODULE_0__["useState"])(false); // Determine the status of the langpack installation.
Object(react__WEBPACK_IMPORTED_MODULE_0__["useEffect"])(() => {
if (isAwaitingLangpack && langPackInstallPhase !== "installing") {
window.AWSetRequestedLocales(negotiatedLanguage.requestSystemLocales);
requestAnimationFrame(() => {
handleAction( // Simulate the click event.
{
currentTarget: {
value: "download_complete"
}
});
});
}
}, [isAwaitingLangpack, langPackInstallPhase]); // The message args are the localized language names.
const withMessageArgs = obj => {
const displayName = negotiatedLanguage === null || negotiatedLanguage === void 0 ? void 0 : negotiatedLanguage.displayName;
if (displayName) {
return { ...obj,
args: { ...obj.args,
negotiatedLanguage: displayName
}
};
}
return obj;
};
let showWaitingScreen = false;
let showPreloadingScreen = false;
let showReadyScreen = false;
if (isAwaitingLangpack && langPackInstallPhase !== "installed") {
showWaitingScreen = true;
} else if (langPackInstallPhase === "before-installation") {
showPreloadingScreen = true;
} else {
showReadyScreen = true;
} // Use {display: "none"} rather than if statements to prevent layout thrashing with
// the localized text elements rendering as blank, then filling in the text.
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_0___default.a.Fragment, null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
style: {
display: showPreloadingScreen ? "block" : "none"
}
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", {
className: "primary",
value: "primary_button",
disabled: true,
type: "button"
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("img", {
className: "language-loader",
src: "chrome://browser/skin/tabbrowser/tab-connecting.png",
alt: ""
}), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
text: content.languageSwitcher.waiting
})), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
className: "secondary-cta"
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
text: content.languageSwitcher.skip
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", {
value: "decline_waiting",
type: "button",
className: "secondary text-link",
onClick: handleAction
})))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
style: {
display: showWaitingScreen ? "block" : "none"
}
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", {
className: "primary",
value: "primary_button",
disabled: true,
type: "button"
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("img", {
className: "language-loader",
src: "chrome://browser/skin/tabbrowser/tab-connecting.png",
alt: ""
}), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
text: withMessageArgs(content.languageSwitcher.downloading)
})), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
className: "secondary-cta"
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
text: content.languageSwitcher.cancel
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", {
type: "button",
className: "secondary text-link",
onClick: () => {
setIsAwaitingLangpack(false);
handleAction({
currentTarget: {
value: "cancel_waiting"
}
});
}
})))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
style: {
display: showReadyScreen ? "block" : "none"
}
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
text: withMessageArgs(content.languageSwitcher.switch)
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", {
className: "primary",
value: "primary_button",
onClick: () => {
_lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_2__["AboutWelcomeUtils"].sendActionTelemetry(messageId, "download_langpack");
setIsAwaitingLangpack(true);
}
}))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
className: "secondary-cta"
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__["Localized"], {
text: content.languageSwitcher.not_now
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("button", {
type: "button",
className: "secondary text-link",
value: "decline",
onClick: handleAction
})))));
}
/***/ }),
/* 10 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "BASE_PARAMS", function() { return BASE_PARAMS; });
@ -1252,7 +1536,7 @@ function addUtmParams(url, utmTerm) {
}
/***/ }),
/* 10 */
/* 11 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
@ -1262,7 +1546,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _lib_aboutwelcome_utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5);
/* harmony import */ var _MultiStageProtonScreen__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6);
/* harmony import */ var _asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(9);
/* harmony import */ var _asrouter_templates_FirstRun_addUtmParams__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(10);
/* 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/. */

Просмотреть файл

@ -211,6 +211,20 @@ body[lwt-newtab-brighttext] {
.onboardingContainer .welcomeZap .zap.long::after {
background-image: url("chrome://activity-stream/content/data/content/assets/long-zap.svg");
}
.onboardingContainer .language-loader {
filter: invert(1);
margin-inline-end: 10px;
position: relative;
top: 3px;
width: 16px;
height: 16px;
margin-top: -6px;
}
@media (prefers-color-scheme: dark) {
.onboardingContainer .language-loader {
filter: invert(0);
}
}
.onboardingContainer .tiles-theme-container {
display: flex;
flex-direction: column;

Просмотреть файл

@ -102,8 +102,36 @@ const DEFAULT_WELCOME_CONTENT = {
},
},
{
id: "AW_IMPORT_SETTINGS",
id: "AW_LANGUAGE_MISMATCH",
order: 2,
content: {
title: { string_id: "onboarding-live-language-header" },
subtitle: { string_id: "onboarding-live-language-subtitle" },
has_noodles: true,
languageSwitcher: {
switch: {
string_id: "onboarding-live-language-switch-button-label",
},
downloading: {
string_id: "onboarding-live-language-button-label-downloading",
},
cancel: {
string_id: "onboarding-live-language-secondary-cancel-download",
},
not_now: {
string_id: "onboarding-live-language-not-now-button-label",
},
waiting: { string_id: "onboarding-live-language-waiting-button" },
skip: { string_id: "onboarding-live-language-skip-button-label" },
action: {
navigate: true,
},
},
},
},
{
id: "AW_IMPORT_SETTINGS",
order: 3,
content: {
title: {
string_id: "mr1-onboarding-import-header",
@ -135,7 +163,7 @@ const DEFAULT_WELCOME_CONTENT = {
},
{
id: "AW_CHOOSE_THEME",
order: 3,
order: 4,
content: {
title: {
string_id: "mr1-onboarding-theme-header",
@ -411,6 +439,25 @@ async function prepareContentForReact(content) {
)?.content.help_text.text;
}
if (content.languageMismatchEnabled) {
const screen = content?.screens?.find(s => s.id === "AW_LANGUAGE_MISMATCH");
if (screen) {
// Add the display names for the OS and Firefox languages, like "American English".
const { appAndSystemLocaleInfo } = content;
function addMessageArgs(obj) {
for (const value of Object.values(obj)) {
if (value?.string_id) {
value.args = appAndSystemLocaleInfo.displayNames;
}
}
}
addMessageArgs(screen.content.languageSwitcher);
addMessageArgs(screen.content);
}
} else {
removeScreens(screen => screen.id === "AW_LANGUAGE_MISMATCH");
}
return content;
}

Просмотреть файл

@ -79,6 +79,7 @@ class AboutWelcome extends React.PureComponent {
utm_term={props.UTMTerm}
transitions={props.transitions}
backdrop={props.backdrop}
appAndSystemLocaleInfo={props.appAndSystemLocaleInfo}
/>
);
}

Просмотреть файл

@ -230,6 +230,22 @@ body {
}
}
.language-loader {
filter: invert(1);
margin-inline-end: 10px;
position: relative;
top: 3px;
width: 16px;
height: 16px;
margin-top: -6px;
}
@media (prefers-color-scheme: dark) {
.language-loader {
filter: invert(0);
}
}
.tiles-theme-container {
display: flex;
flex-direction: column;

Просмотреть файл

@ -0,0 +1,277 @@
/* 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/. */
import React, { useState, useEffect } from "react";
import { Localized } from "./MSLocalized";
import { AboutWelcomeUtils } from "../../lib/aboutwelcome-utils";
/**
* The language switcher implements a hook that should be placed at a higher level
* than the actual language switcher component, as it needs to preemptively fetch
* and install langpacks for the user if there is a language mismatch screen.
*/
export function useLanguageSwitcher(
appAndSystemLocaleInfo,
screens,
screenIndex,
setScreenIndex
) {
const languageMismatchScreenIndex = screens.findIndex(
({ id }) => id === "AW_LANGUAGE_MISMATCH"
);
const screen = screens[languageMismatchScreenIndex];
// If there is a mismatch, then Firefox can negotiate a better langpack to offer
// the user.
const [negotiatedLanguage, setNegotiatedLanguage] = useState(null);
useEffect(
function getNegotiatedLanguage() {
if (!appAndSystemLocaleInfo) {
return;
}
if (appAndSystemLocaleInfo.matchType !== "language-mismatch") {
// There is no language mismatch, so there is no need to negotiate a langpack.
return;
}
(async () => {
const langPack = await window.AWNegotiateLangPackForLanguageMismatch(
appAndSystemLocaleInfo
);
if (langPack) {
// Convert the BCP 47 identifiers into the proper display names.
// e.g. "fr-CA" -> "Canadian French".
const displayNames = new Intl.DisplayNames(
appAndSystemLocaleInfo.appLocaleRaw,
{ type: "language" }
);
setNegotiatedLanguage({
displayName: displayNames.of(langPack.target_locale),
langPack,
requestSystemLocales: [
langPack.target_locale,
appAndSystemLocaleInfo.appLocaleRaw,
],
});
} else {
setNegotiatedLanguage({
displayName: null,
langPack: null,
requestSystemLocales: null,
});
}
})();
},
[appAndSystemLocaleInfo]
);
/**
* @type {
* "before-installation"
* | "installing"
* | "installed"
* | "installation-error"
* | "none-available"
* }
*/
const [langPackInstallPhase, setLangPackInstallPhase] = useState(
"before-installation"
);
useEffect(
function ensureLangPackInstalled() {
if (!negotiatedLanguage) {
// There are no negotiated languages to download yet.
return;
}
setLangPackInstallPhase("installing");
window.AWEnsureLangPackInstalled(negotiatedLanguage.langPack).then(
() => {
setLangPackInstallPhase("installed");
},
error => {
console.error(error);
setLangPackInstallPhase("installation-error");
}
);
},
[negotiatedLanguage]
);
const shouldHideLanguageSwitcher =
screen && appAndSystemLocaleInfo?.matchType !== "language-mismatch";
const [languageFilteredScreens, setLanguageFilteredScreens] = useState(
screens
);
useEffect(
function filterScreen() {
if (shouldHideLanguageSwitcher || negotiatedLanguage?.langPack === null) {
if (screenIndex > languageMismatchScreenIndex) {
setScreenIndex(screenIndex - 1);
}
setLanguageFilteredScreens(
screens.filter(s => s.id !== "AW_LANGUAGE_MISMATCH")
);
} else {
setLanguageFilteredScreens(screens);
}
},
[screens, negotiatedLanguage]
);
return {
negotiatedLanguage,
langPackInstallPhase,
languageFilteredScreens,
};
}
/**
* The language switcher is a separate component as it needs to perform some asynchronous
* network actions such as retrieving the list of langpacks available, and downloading
* a new langpack. On a fast connection, this won't be noticeable, but on slow or unreliable
* internet this may fail for a user.
*/
export function LanguageSwitcher(props) {
const {
content,
handleAction,
negotiatedLanguage,
langPackInstallPhase,
messageId,
} = props;
const [isAwaitingLangpack, setIsAwaitingLangpack] = useState(false);
// Determine the status of the langpack installation.
useEffect(() => {
if (isAwaitingLangpack && langPackInstallPhase !== "installing") {
window.AWSetRequestedLocales(negotiatedLanguage.requestSystemLocales);
requestAnimationFrame(() => {
handleAction(
// Simulate the click event.
{ currentTarget: { value: "download_complete" } }
);
});
}
}, [isAwaitingLangpack, langPackInstallPhase]);
// The message args are the localized language names.
const withMessageArgs = obj => {
const displayName = negotiatedLanguage?.displayName;
if (displayName) {
return {
...obj,
args: {
...obj.args,
negotiatedLanguage: displayName,
},
};
}
return obj;
};
let showWaitingScreen = false;
let showPreloadingScreen = false;
let showReadyScreen = false;
if (isAwaitingLangpack && langPackInstallPhase !== "installed") {
showWaitingScreen = true;
} else if (langPackInstallPhase === "before-installation") {
showPreloadingScreen = true;
} else {
showReadyScreen = true;
}
// Use {display: "none"} rather than if statements to prevent layout thrashing with
// the localized text elements rendering as blank, then filling in the text.
return (
<>
<div style={{ display: showPreloadingScreen ? "block" : "none" }}>
<button
className="primary"
value="primary_button"
disabled={true}
type="button"
>
<img
className="language-loader"
src="chrome://browser/skin/tabbrowser/tab-connecting.png"
alt=""
/>
<Localized text={content.languageSwitcher.waiting} />
</button>
<div className="secondary-cta">
<Localized text={content.languageSwitcher.skip}>
<button
value="decline_waiting"
type="button"
className="secondary text-link"
onClick={handleAction}
/>
</Localized>
</div>
</div>
<div style={{ display: showWaitingScreen ? "block" : "none" }}>
<button
className="primary"
value="primary_button"
disabled={true}
type="button"
>
<img
className="language-loader"
src="chrome://browser/skin/tabbrowser/tab-connecting.png"
alt=""
/>
<Localized
text={withMessageArgs(content.languageSwitcher.downloading)}
/>
</button>
<div className="secondary-cta">
<Localized text={content.languageSwitcher.cancel}>
<button
type="button"
className="secondary text-link"
onClick={() => {
setIsAwaitingLangpack(false);
handleAction({
currentTarget: { value: "cancel_waiting" },
});
}}
/>
</Localized>
</div>
</div>
<div style={{ display: showReadyScreen ? "block" : "none" }}>
<div>
<Localized text={withMessageArgs(content.languageSwitcher.switch)}>
<button
className="primary"
value="primary_button"
onClick={() => {
AboutWelcomeUtils.sendActionTelemetry(
messageId,
"download_langpack"
);
setIsAwaitingLangpack(true);
}}
/>
</Localized>
</div>
<div className="secondary-cta">
<Localized text={content.languageSwitcher.not_now}>
<button
type="button"
className="secondary text-link"
value="decline"
onClick={handleAction}
/>
</Localized>
</div>
</div>
</>
);
}

Просмотреть файл

@ -6,6 +6,7 @@ import React, { useState, useEffect, useRef } from "react";
import { Localized } from "./MSLocalized";
import { AboutWelcomeUtils } from "../../lib/aboutwelcome-utils";
import { MultiStageProtonScreen } from "./MultiStageProtonScreen";
import { useLanguageSwitcher } from "./LanguageSwitcher";
import {
BASE_PARAMS,
addUtmParams,
@ -15,10 +16,12 @@ import {
const TRANSITION_OUT_TIME = 1000;
export const MultiStageAboutWelcome = props => {
let { screens } = props;
const [index, setScreenIndex] = useState(0);
useEffect(() => {
// Send impression ping when respective screen first renders
props.screens.forEach((screen, order) => {
screens.forEach((screen, order) => {
if (index === order) {
AboutWelcomeUtils.sendImpressionTelemetry(
`${props.message_id}_${order}_${screen.id}`
@ -37,7 +40,7 @@ export const MultiStageAboutWelcome = props => {
// or last screen index if a user navigates by pressing back
// button from about:home
const handler = ({ state }) =>
setScreenIndex(Math.min(state, props.screens.length - 1));
setScreenIndex(Math.min(state, screens.length - 1));
// Handle page load, e.g., going back to about:welcome from about:home
handler(window.history);
@ -81,7 +84,7 @@ export const MultiStageAboutWelcome = props => {
// Actually move forwards after all transitions finish.
setTimeout(
() => {
if (index < props.screens.length - 1) {
if (index < screens.length - 1) {
setTransition(props.transitions ? "in" : "");
setScreenIndex(prevState => prevState + 1);
} else {
@ -140,13 +143,26 @@ export const MultiStageAboutWelcome = props => {
s => s.content.position !== "corner"
);
const {
negotiatedLanguage,
langPackInstallPhase,
languageFilteredScreens,
} = useLanguageSwitcher(
props.appAndSystemLocaleInfo,
screens,
index,
setScreenIndex
);
screens = languageFilteredScreens;
return (
<React.Fragment>
<div
className={`outer-wrapper onboardingContainer proton transition-${transition}`}
style={props.backdrop ? { background: props.backdrop } : {}}
>
{props.screens.map((screen, order) => {
{screens.map((screen, order) => {
const isFirstCenteredScreen =
screen.content.position !== "corner" &&
screen.order === centeredScreens[0].order;
@ -157,7 +173,7 @@ export const MultiStageAboutWelcome = props => {
<WelcomeScreen
key={screen.id + order}
id={screen.id}
totalNumberOfScreens={props.screens.length}
totalNumberOfScreens={screens.length}
isFirstCenteredScreen={isFirstCenteredScreen}
isLastCenteredScreen={isLastCenteredScreen}
order={order}
@ -171,6 +187,8 @@ export const MultiStageAboutWelcome = props => {
initialTheme={initialTheme}
setActiveTheme={setActiveTheme}
autoAdvance={screen.auto_advance}
negotiatedLanguage={negotiatedLanguage}
langPackInstallPhase={langPackInstallPhase}
/>
) : null;
})}
@ -248,7 +266,11 @@ export class WelcomeScreen extends React.PureComponent {
async handleAction(event) {
let { props } = this;
let { value } = event.currentTarget;
let targetContent = props.content[value] || props.content.tiles;
let targetContent =
props.content[value] ||
props.content.tiles ||
props.content.languageSwitcher;
if (!(targetContent && targetContent.action)) {
return;
}
@ -295,7 +317,11 @@ export class WelcomeScreen extends React.PureComponent {
order={this.props.order}
activeTheme={this.props.activeTheme}
totalNumberOfScreens={this.props.totalNumberOfScreens - 1}
appAndSystemLocaleInfo={this.props.appAndSystemLocaleInfo}
negotiatedLanguage={this.props.negotiatedLanguage}
langPackInstallPhase={this.props.langPackInstallPhase}
handleAction={this.handleAction}
messageId={this.props.messageId}
isFirstCenteredScreen={this.props.isFirstCenteredScreen}
isLastCenteredScreen={this.props.isLastCenteredScreen}
autoAdvance={this.props.autoAdvance}

Просмотреть файл

@ -7,6 +7,7 @@ import { Localized } from "./MSLocalized";
import { Colorways } from "./Colorways";
import { Themes } from "./Themes";
import { SecondaryCTA, StepsIndicator } from "./MultiStageAboutWelcome";
import { LanguageSwitcher } from "./LanguageSwitcher";
export const MultiStageProtonScreen = props => {
const { autoAdvance, handleAction, order } = props;
@ -38,6 +39,9 @@ export const MultiStageProtonScreen = props => {
isRtamo={props.isRtamo}
isTheme={props.isTheme}
iconURL={props.iconURL}
messageId={props.messageId}
negotiatedLanguage={props.negotiatedLanguage}
langPackInstallPhase={props.langPackInstallPhase}
/>
);
};
@ -114,6 +118,18 @@ export class ProtonScreen extends React.PureComponent {
);
}
renderLanguageSwitcher() {
return this.props.content.languageSwitcher ? (
<LanguageSwitcher
content={this.props.content}
handleAction={this.props.handleAction}
negotiatedLanguage={this.props.negotiatedLanguage}
langPackInstallPhase={this.props.langPackInstallPhase}
messageId={this.props.messageId}
/>
) : null;
}
render() {
const {
autoAdvance,
@ -201,12 +217,14 @@ export class ProtonScreen extends React.PureComponent {
<h2
data-l10n-args={JSON.stringify({
"addon-name": this.props.addonName,
...this.props.appAndSystemLocaleInfo?.displayNames,
})}
/>
</Localized>
) : null}
</div>
{this.renderContentTiles()}
{this.renderLanguageSwitcher()}
<div>
<Localized
text={
@ -216,6 +234,7 @@ export class ProtonScreen extends React.PureComponent {
<button
className="primary"
value="primary_button"
disabled={content.primary_button?.disabled === true}
onClick={this.props.handleAction}
/>
</Localized>

Просмотреть файл

@ -188,6 +188,13 @@ module.exports = function(config) {
functions: 96,
branches: 70,
},
"content-src/aboutwelcome/components/LanguageSwitcher.jsx": {
// This file is covered by the mochitest: browser_aboutwelcome_multistage_languageSwitcher.js
statements: 0,
lines: 0,
functions: 0,
branches: 0,
},
"content-src/aboutwelcome/**/*.jsx": {
statements: 62,
lines: 60,

Просмотреть файл

@ -16,12 +16,14 @@ prefs =
browser.newtabpage.activity-stream.feeds.section.topstories=true
browser.newtabpage.activity-stream.feeds.section.topstories.options={"provider_name":""}
messaging-system.log=all
intl.multilingual.aboutWelcome.languageMismatchEnabled=false
[browser_aboutwelcome_configurable_ui.js]
[browser_aboutwelcome_focus.js]
[browser_aboutwelcome_multistage_default.js]
[browser_aboutwelcome_multistage_primary.js]
[browser_aboutwelcome_multistage_experimentAPI.js]
[browser_aboutwelcome_multistage_languageSwitcher.js]
[browser_aboutwelcome_rtamo.js]
skip-if = (os == "linux") # Test setup only implemented for OSX and Windows
[browser_aboutwelcome_attribution.js]

Просмотреть файл

@ -0,0 +1,572 @@
"use strict";
const { getAddonAndLocalAPIsMocker } = ChromeUtils.import(
"resource://testing-common/LangPackMatcherTestUtils.jsm"
);
const sandbox = sinon.createSandbox();
const mockAddonAndLocaleAPIs = getAddonAndLocalAPIsMocker(this, sandbox);
add_task(function initSandbox() {
registerCleanupFunction(() => {
sandbox.restore();
});
});
/**
* Spy specifically on the button click telemetry.
*
* The returned function flushes the spy of all of the matching button click events, and
* returns the events.
* @returns {() => TelemetryEvents[]}
*/
async function spyOnTelemetryButtonClicks(browser) {
let aboutWelcomeActor = await getAboutWelcomeParent(browser);
sandbox.spy(aboutWelcomeActor, "onContentMessage");
return () => {
const result = aboutWelcomeActor.onContentMessage
.getCalls()
.filter(
call =>
call.args[0] === "AWPage:TELEMETRY_EVENT" &&
call.args[1]?.event === "CLICK_BUTTON"
)
// The second argument is the telemetry event.
.map(call => call.args[1]);
aboutWelcomeActor.onContentMessage.resetHistory();
return result;
};
}
async function openAboutWelcome() {
await pushPrefs([
"intl.multilingual.aboutWelcome.languageMismatchEnabled",
true,
]);
await setAboutWelcomePref(true);
// Stub out the doesAppNeedPin to false so the about:welcome pages do not attempt
// to pin the app.
const { ShellService } = ChromeUtils.import(
"resource:///modules/ShellService.jsm"
);
sandbox.stub(ShellService, "doesAppNeedPin").returns(false);
info("Opening about:welcome");
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"about:welcome",
true
);
registerCleanupFunction(async () => {
BrowserTestUtils.removeTab(tab);
});
return {
browser: tab.linkedBrowser,
flushClickTelemetry: await spyOnTelemetryButtonClicks(tab.linkedBrowser),
};
}
async function clickVisibleButton(browser, selector) {
// eslint-disable-next-line no-shadow
await ContentTask.spawn(browser, { selector }, async ({ selector }) => {
function getVisibleElement() {
for (const el of content.document.querySelectorAll(selector)) {
if (el.offsetParent !== null) {
return el;
}
}
return null;
}
await ContentTaskUtils.waitForCondition(getVisibleElement, selector);
getVisibleElement().click();
});
}
/**
* Test that selectors are present and visible.
*/
async function testScreenContent(
browser,
name,
expectedSelectors = [],
unexpectedSelectors = []
) {
await ContentTask.spawn(
browser,
{ expectedSelectors, name, unexpectedSelectors },
async ({
expectedSelectors: expected,
name: experimentName,
unexpectedSelectors: unexpected,
}) => {
function selectorIsVisible(selector) {
const el = content.document.querySelector(selector);
// The offsetParent will be null if element is hidden through "display: none;"
return el && el.offsetParent !== null;
}
for (let selector of expected) {
await ContentTaskUtils.waitForCondition(
() => selectorIsVisible(selector),
`Should render ${selector} in ${experimentName}`
);
}
for (let selector of unexpected) {
ok(
!selectorIsVisible(selector),
`Should not render ${selector} in ${experimentName}`
);
}
}
);
}
/**
* Report telemetry mismatches nicely.
*/
function eventsMatch(
actualEvents,
expectedEvents,
message = "Telemetry events match"
) {
if (actualEvents.length !== expectedEvents.length) {
console.error("Events do not match");
console.error("Actual: ", JSON.stringify(actualEvents, null, 2));
console.error("Expected: ", JSON.stringify(expectedEvents, null, 2));
}
for (let i = 0; i < actualEvents.length; i++) {
const actualEvent = JSON.stringify(actualEvents[i], null, 2);
const expectedEvent = JSON.stringify(expectedEvents[i], null, 2);
if (actualEvent !== expectedEvent) {
console.error("Events do not match");
dump(`Actual: ${actualEvent}`);
dump("\n");
dump(`Expected: ${expectedEvent}`);
dump("\n");
}
ok(actualEvent === expectedEvent, message);
}
}
const liveLanguageSwitchSelectors = [
".screen-1",
`[data-l10n-id*="onboarding-live-language"]`,
`[data-l10n-id="onboarding-live-language-header"]`,
];
/**
* Accept the about:welcome offer to change the Firefox language when
* there is a mismatch between the operating system language and the Firefox
* language.
*/
add_task(async function test_aboutwelcome_languageSwitcher_accept() {
sandbox.restore();
const { resolveLangPacks, resolveInstaller } = mockAddonAndLocaleAPIs({
systemLocale: "es-ES",
appLocale: "en-US",
});
const { browser, flushClickTelemetry } = await openAboutWelcome();
info("Clicking the primary button to start the onboarding process.");
await clickVisibleButton(browser, "button.primary");
await testScreenContent(
browser,
"Live language switching (waiting for languages)",
// Expected selectors:
[
...liveLanguageSwitchSelectors,
`[data-l10n-id="onboarding-live-language-header"]`,
`button[disabled] [data-l10n-id="onboarding-live-language-waiting-button"]`,
`[data-l10n-id="onboarding-live-language-skip-button-label"]`,
],
// Unexpected selectors:
[]
);
// Ignore the telemetry of the initial welcome screen.
flushClickTelemetry();
resolveLangPacks(["es-MX", "es-ES", "fr-FR"]);
await testScreenContent(
browser,
"Live language switching, asking for a language",
// Expected selectors:
[
...liveLanguageSwitchSelectors,
`[data-l10n-id="onboarding-live-language-switch-button-label"]`,
`[data-l10n-id="onboarding-live-language-not-now-button-label"]`,
],
// Unexpected selectors:
[
`button[disabled] [data-l10n-id="onboarding-live-language-waiting-button"]`,
`[data-l10n-id="onboarding-live-language-skip-button-label"]`,
]
);
info("Clicking the primary button to view language switching page.");
await clickVisibleButton(browser, "button.primary");
await testScreenContent(
browser,
"Live language switching, waiting for langpack to download",
// Expected selectors:
[
...liveLanguageSwitchSelectors,
`[data-l10n-id="onboarding-live-language-button-label-downloading"]`,
`[data-l10n-id="onboarding-live-language-secondary-cancel-download"]`,
],
// Unexpected selectors:
[
`button[disabled] [data-l10n-id="onboarding-live-language-waiting-button"]`,
]
);
eventsMatch(flushClickTelemetry(), [
{
event: "CLICK_BUTTON",
event_context: {
source: "download_langpack",
page: "about:welcome",
},
message_id: "DEFAULT_ABOUTWELCOME_PROTON_1_AW_LANGUAGE_MISMATCH",
},
]);
await resolveInstaller();
await testScreenContent(
browser,
"Language selection declined",
// Expected selectors:
[`.screen-2`],
// Unexpected selectors:
liveLanguageSwitchSelectors
);
eventsMatch(flushClickTelemetry(), [
{
event: "CLICK_BUTTON",
event_context: {
source: "download_complete",
page: "about:welcome",
},
message_id: "DEFAULT_ABOUTWELCOME_PROTON_1_AW_LANGUAGE_MISMATCH",
},
]);
});
/**
* Accept the about:welcome offer to change the Firefox language when
* there is a mismatch between the operating system language and the Firefox
* language.
*/
add_task(async function test_aboutwelcome_languageSwitcher_accept() {
sandbox.restore();
const { resolveLangPacks, resolveInstaller } = mockAddonAndLocaleAPIs({
systemLocale: "es-ES",
appLocale: "en-US",
});
const { browser, flushClickTelemetry } = await openAboutWelcome();
info("Clicking the primary button to start the onboarding process.");
await clickVisibleButton(browser, "button.primary");
await testScreenContent(
browser,
"Live language switching (waiting for languages)",
// Expected selectors:
[
...liveLanguageSwitchSelectors,
`[data-l10n-id="onboarding-live-language-header"]`,
`button[disabled] [data-l10n-id="onboarding-live-language-waiting-button"]`,
`[data-l10n-id="onboarding-live-language-skip-button-label"]`,
],
// Unexpected selectors:
[]
);
// Ignore the telemetry of the initial welcome screen.
flushClickTelemetry();
resolveLangPacks(["es-MX", "es-ES", "fr-FR"]);
await testScreenContent(
browser,
"Live language switching, asking for a language",
// Expected selectors:
[
...liveLanguageSwitchSelectors,
`[data-l10n-id="onboarding-live-language-switch-button-label"]`,
`[data-l10n-id="onboarding-live-language-not-now-button-label"]`,
],
// Unexpected selectors:
[
`button[disabled] [data-l10n-id="onboarding-live-language-waiting-button"]`,
`[data-l10n-id="onboarding-live-language-skip-button-label"]`,
]
);
info("Clicking the primary button to view language switching page.");
await clickVisibleButton(browser, "button.primary");
await testScreenContent(
browser,
"Live language switching, waiting for langpack to download",
// Expected selectors:
[
...liveLanguageSwitchSelectors,
`[data-l10n-id="onboarding-live-language-button-label-downloading"]`,
`[data-l10n-id="onboarding-live-language-secondary-cancel-download"]`,
],
// Unexpected selectors:
[
`button[disabled] [data-l10n-id="onboarding-live-language-waiting-button"]`,
]
);
eventsMatch(flushClickTelemetry(), [
{
event: "CLICK_BUTTON",
event_context: {
source: "download_langpack",
page: "about:welcome",
},
message_id: "DEFAULT_ABOUTWELCOME_PROTON_1_AW_LANGUAGE_MISMATCH",
},
]);
await resolveInstaller();
await testScreenContent(
browser,
"Language selection declined",
// Expected selectors:
[`.screen-2`],
// Unexpected selectors:
liveLanguageSwitchSelectors
);
});
/**
* Test declining the about:welcome offer to change the Firefox language when
* there is a mismatch between the operating system language and the Firefox
* language.
*/
add_task(async function test_aboutwelcome_languageSwitcher_decline() {
sandbox.restore();
const { resolveLangPacks, resolveInstaller } = mockAddonAndLocaleAPIs({
systemLocale: "es-ES",
appLocale: "en-US",
});
const { browser, flushClickTelemetry } = await openAboutWelcome();
info("Clicking the primary button to view language switching page.");
await clickVisibleButton(browser, "button.primary");
await testScreenContent(
browser,
"Live language switching (waiting for languages)",
// Expected selectors:
[
...liveLanguageSwitchSelectors,
`[data-l10n-id="onboarding-live-language-header"]`,
`button[disabled] [data-l10n-id="onboarding-live-language-waiting-button"]`,
`[data-l10n-id="onboarding-live-language-skip-button-label"]`,
],
// Unexpected selectors:
[]
);
// Ignore the telemetry of the initial welcome screen.
flushClickTelemetry();
resolveLangPacks(["es-MX", "es-ES", "fr-FR"]);
resolveInstaller();
await testScreenContent(
browser,
"Live language switching, asking for a language",
// Expected selectors:
[
...liveLanguageSwitchSelectors,
`[data-l10n-id="onboarding-live-language-switch-button-label"]`,
`[data-l10n-id="onboarding-live-language-not-now-button-label"]`,
],
// Unexpected selectors:
[
`button[disabled] [data-l10n-id="onboarding-live-language-waiting-button"]`,
`[data-l10n-id="onboarding-live-language-skip-button-label"]`,
]
);
info("Clicking the secondary button to skip installing the langpack.");
await clickVisibleButton(browser, "button.secondary");
await testScreenContent(
browser,
"Language selection declined",
// Expected selectors:
[`.screen-2`],
// Unexpected selectors:
liveLanguageSwitchSelectors
);
eventsMatch(flushClickTelemetry(), [
{
event: "CLICK_BUTTON",
event_context: {
source: "decline",
page: "about:welcome",
},
message_id: "DEFAULT_ABOUTWELCOME_PROTON_1_AW_LANGUAGE_MISMATCH",
},
]);
});
/**
* Ensure the langpack can be installed before the user gets to the language screen.
*/
add_task(async function test_aboutwelcome_languageSwitcher_asyncCalls() {
sandbox.restore();
const {
resolveLangPacks,
resolveInstaller,
mockable,
} = mockAddonAndLocaleAPIs({
systemLocale: "es-ES",
appLocale: "en-US",
});
await openAboutWelcome();
info("Waiting for getAvailableLangpacks to be called.");
await TestUtils.waitForCondition(
() => mockable.getAvailableLangpacks.called,
"getAvailableLangpacks called once"
);
ok(mockable.installLangPack.notCalled);
resolveLangPacks(["es-MX", "es-ES", "fr-FR"]);
await TestUtils.waitForCondition(
() => mockable.installLangPack.called,
"installLangPack was called once"
);
ok(mockable.getAvailableLangpacks.called);
resolveInstaller();
});
/**
* Test when AMO does not have a matching language.
*/
add_task(async function test_aboutwelcome_languageSwitcher_noMatch() {
sandbox.restore();
const { resolveLangPacks } = mockAddonAndLocaleAPIs({
systemLocale: "tlh", // Klingon
appLocale: "en-US",
});
const { browser } = await openAboutWelcome();
info("Clicking the primary button to start installing the langpack.");
await clickVisibleButton(browser, "button.primary");
// Klingon is not supported.
resolveLangPacks(["es-MX", "es-ES", "fr-FR"]);
await testScreenContent(
browser,
"Language selection skipped",
// Expected selectors:
[`.screen-1`],
// Unexpected selectors:
[
`[data-l10n-id*="onboarding-live-language"]`,
`[data-l10n-id="onboarding-live-language-header"]`,
]
);
});
/**
* Test hitting the cancel button when waiting on a langpack.
*/
add_task(async function test_aboutwelcome_languageSwitcher_cancelWaiting() {
sandbox.restore();
const { resolveLangPacks, resolveInstaller } = mockAddonAndLocaleAPIs({
systemLocale: "es-ES",
appLocale: "en-US",
});
const { browser, flushClickTelemetry } = await openAboutWelcome();
info("Clicking the primary button to start the onboarding process.");
await clickVisibleButton(browser, "button.primary");
resolveLangPacks(["es-MX", "es-ES", "fr-FR"]);
await testScreenContent(
browser,
"Live language switching, asking for a language",
// Expected selectors:
liveLanguageSwitchSelectors,
// Unexpected selectors:
[]
);
info("Clicking the primary button to view language switching page.");
await clickVisibleButton(browser, "button.primary");
await testScreenContent(
browser,
"Live language switching, waiting for langpack to download",
// Expected selectors:
[
...liveLanguageSwitchSelectors,
`[data-l10n-id="onboarding-live-language-button-label-downloading"]`,
`[data-l10n-id="onboarding-live-language-secondary-cancel-download"]`,
],
// Unexpected selectors:
[
`button[disabled] [data-l10n-id="onboarding-live-language-waiting-button"]`,
]
);
// Ignore all the telemetry up to this point.
flushClickTelemetry();
info("Cancel the request for the language");
await clickVisibleButton(browser, "button.secondary");
await testScreenContent(
browser,
"Language selection declined waiting",
// Expected selectors:
[`.screen-2`],
// Unexpected selectors:
liveLanguageSwitchSelectors
);
eventsMatch(flushClickTelemetry(), [
{
event: "CLICK_BUTTON",
event_context: {
source: "cancel_waiting",
page: "about:welcome",
},
message_id: "DEFAULT_ABOUTWELCOME_PROTON_1_AW_LANGUAGE_MISMATCH",
},
]);
await resolveInstaller();
is(flushClickTelemetry().length, 0);
});

Просмотреть файл

@ -74,28 +74,28 @@ describe("MultiStageAboutWelcomeProton module", () => {
);
assert.propertyVal(data.screens[0], "id", "AW_PIN_FIREFOX");
assert.propertyVal(data.screens[1], "id", "AW_SET_DEFAULT");
assert.lengthOf(data.screens, getData().screens.length);
assert.lengthOf(data.screens, getData().screens.length - 1);
});
it("should keep 'pin' and remove 'default' if already default", async () => {
const data = await prepConfig({ needPin: true });
assert.propertyVal(data.screens[0], "id", "AW_PIN_FIREFOX");
assert.propertyVal(data.screens[1], "id", "AW_IMPORT_SETTINGS");
assert.lengthOf(data.screens, getData().screens.length - 1);
assert.lengthOf(data.screens, getData().screens.length - 2);
});
it("should switch to 'default' if already pinned", async () => {
const data = await prepConfig({ needDefault: true });
assert.propertyVal(data.screens[0], "id", "AW_ONLY_DEFAULT");
assert.propertyVal(data.screens[1], "id", "AW_IMPORT_SETTINGS");
assert.lengthOf(data.screens, getData().screens.length - 1);
assert.lengthOf(data.screens, getData().screens.length - 2);
});
it("should switch to 'start' if already pinned and default", async () => {
const data = await prepConfig();
assert.propertyVal(data.screens[0], "id", "AW_GET_STARTED");
assert.propertyVal(data.screens[1], "id", "AW_IMPORT_SETTINGS");
assert.lengthOf(data.screens, getData().screens.length - 1);
assert.lengthOf(data.screens, getData().screens.length - 2);
});
it("should have a FxA button", async () => {
const data = await prepConfig();

Просмотреть файл

@ -222,3 +222,24 @@ mr2-onboarding-default-theme-label = Explore default themes.
mr2-onboarding-thank-you-header = Thank you for choosing us
mr2-onboarding-thank-you-text = { -brand-short-name } is an independent browser backed by a non-profit. Together, were making the web safer, healthier, and more private.
mr2-onboarding-start-browsing-button-label = Start browsing
## Multistage live language reloading onboarding strings (about:welcome pages)
##
## The following language names are generated by the browser's Intl.DisplayNames API.
##
## Variables:
## $appLanguage (String) - The name of Firefox's language, e.g. "American English"
## $systemLanguage (String) - The name of the OS's language, e.g. "European Spanish"
## $negotiatedLanguage (String) - The name of the langpack's language, e.g. "European Spanish"
onboarding-live-language-header = Choose Your Language
onboarding-live-language-subtitle = { -brand-short-name } is using { $appLanguage } while your system is using { $systemLanguage }.
onboarding-live-language-switch-button-label = Switch to { $negotiatedLanguage }
onboarding-live-language-button-label-downloading = Downloading the language pack for { $negotiatedLanguage }…
onboarding-live-language-waiting-subtitle = It looks like your system and { -brand-short-name } are using different languages.
onboarding-live-language-waiting-button = Getting available languages…
onboarding-live-language-installing = Installing the language pack for { $negotiatedLanguage }…
onboarding-live-language-secondary-cancel-download = Cancel
onboarding-live-language-not-now-button-label = Not now
onboarding-live-language-skip-button-label = Skip

Просмотреть файл

@ -125,6 +125,12 @@ aboutwelcome:
description: >-
Should the urlbar should be focused when users first land on
about:welcome?
languageMismatchEnabled:
type: boolean
fallbackPref: intl.multilingual.aboutWelcome.languageMismatchEnabled
description: >-
Suggest to change the language on about:welcome when there is a mismatch with
the OS.
transitions:
type: boolean
description: Enable transition effect between screens