Merge mozilla-central to mozilla-beta. a=same-version-merge l10n=same-version-merge DONTBUILD because busted
authorSebastian Hengst <archaeopteryx@coole-files.de>
Thu, 04 Jul 2019 17:15:56 +0200
changeset 544056 ebb510a784b8c1204db1d6a6cc1c5b6ca86e081e
parent 543579 75e5f62b44ed3a080f7863606863f1fd5e36b18a (current diff)
parent 544055 6a2bd09a6bf9aada14581e923408d7308479b76d (diff)
child 544057 88e401857465b6f1e97b06b2d0f5be23a8d2e911
push id2131
push userffxbld-merge
push dateMon, 26 Aug 2019 18:30:20 +0000
treeherdermozilla-release@b19ffb3ca153 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssame-version-merge
milestone69.0
Merge mozilla-central to mozilla-beta. a=same-version-merge l10n=same-version-merge DONTBUILD because busted
browser/components/aboutlogins/content/components/copy-to-clipboard-button.css
browser/components/aboutlogins/content/components/copy-to-clipboard-button.js
browser/components/aboutlogins/content/components/reflected-fluent-element.js
browser/components/aboutlogins/tests/chrome/test_reflected_fluent_element.html
browser/components/newtab/bin/locales.js
browser/components/newtab/bin/strings-import.js
browser/components/newtab/locales-src/ach/strings.properties
browser/components/newtab/locales-src/an/strings.properties
browser/components/newtab/locales-src/ar/strings.properties
browser/components/newtab/locales-src/ast/strings.properties
browser/components/newtab/locales-src/az/strings.properties
browser/components/newtab/locales-src/be/strings.properties
browser/components/newtab/locales-src/bg/strings.properties
browser/components/newtab/locales-src/bn/strings.properties
browser/components/newtab/locales-src/br/strings.properties
browser/components/newtab/locales-src/bs/strings.properties
browser/components/newtab/locales-src/ca/strings.properties
browser/components/newtab/locales-src/cak/strings.properties
browser/components/newtab/locales-src/crh/strings.properties
browser/components/newtab/locales-src/cs/strings.properties
browser/components/newtab/locales-src/cy/strings.properties
browser/components/newtab/locales-src/da/strings.properties
browser/components/newtab/locales-src/de/strings.properties
browser/components/newtab/locales-src/dsb/strings.properties
browser/components/newtab/locales-src/el/strings.properties
browser/components/newtab/locales-src/en-CA/strings.properties
browser/components/newtab/locales-src/en-GB/strings.properties
browser/components/newtab/locales-src/en-US/strings.properties
browser/components/newtab/locales-src/eo/strings.properties
browser/components/newtab/locales-src/es-AR/strings.properties
browser/components/newtab/locales-src/es-CL/strings.properties
browser/components/newtab/locales-src/es-ES/strings.properties
browser/components/newtab/locales-src/es-MX/strings.properties
browser/components/newtab/locales-src/et/strings.properties
browser/components/newtab/locales-src/eu/strings.properties
browser/components/newtab/locales-src/fa/strings.properties
browser/components/newtab/locales-src/ff/strings.properties
browser/components/newtab/locales-src/fi/strings.properties
browser/components/newtab/locales-src/fr/strings.properties
browser/components/newtab/locales-src/fy-NL/strings.properties
browser/components/newtab/locales-src/ga-IE/strings.properties
browser/components/newtab/locales-src/gd/strings.properties
browser/components/newtab/locales-src/gl/strings.properties
browser/components/newtab/locales-src/gn/strings.properties
browser/components/newtab/locales-src/gu-IN/strings.properties
browser/components/newtab/locales-src/he/strings.properties
browser/components/newtab/locales-src/hi-IN/strings.properties
browser/components/newtab/locales-src/hr/strings.properties
browser/components/newtab/locales-src/hsb/strings.properties
browser/components/newtab/locales-src/hu/strings.properties
browser/components/newtab/locales-src/hy-AM/strings.properties
browser/components/newtab/locales-src/ia/strings.properties
browser/components/newtab/locales-src/id/strings.properties
browser/components/newtab/locales-src/is/strings.properties
browser/components/newtab/locales-src/it/strings.properties
browser/components/newtab/locales-src/ja-JP-mac/strings.properties
browser/components/newtab/locales-src/ja/strings.properties
browser/components/newtab/locales-src/ka/strings.properties
browser/components/newtab/locales-src/kab/strings.properties
browser/components/newtab/locales-src/kk/strings.properties
browser/components/newtab/locales-src/km/strings.properties
browser/components/newtab/locales-src/kn/strings.properties
browser/components/newtab/locales-src/ko/strings.properties
browser/components/newtab/locales-src/lij/strings.properties
browser/components/newtab/locales-src/lo/strings.properties
browser/components/newtab/locales-src/lt/strings.properties
browser/components/newtab/locales-src/ltg/strings.properties
browser/components/newtab/locales-src/lv/strings.properties
browser/components/newtab/locales-src/mk/strings.properties
browser/components/newtab/locales-src/mr/strings.properties
browser/components/newtab/locales-src/ms/strings.properties
browser/components/newtab/locales-src/my/strings.properties
browser/components/newtab/locales-src/nb-NO/strings.properties
browser/components/newtab/locales-src/ne-NP/strings.properties
browser/components/newtab/locales-src/nl/strings.properties
browser/components/newtab/locales-src/nn-NO/strings.properties
browser/components/newtab/locales-src/oc/strings.properties
browser/components/newtab/locales-src/pa-IN/strings.properties
browser/components/newtab/locales-src/pl/strings.properties
browser/components/newtab/locales-src/pt-BR/strings.properties
browser/components/newtab/locales-src/pt-PT/strings.properties
browser/components/newtab/locales-src/rm/strings.properties
browser/components/newtab/locales-src/ro/strings.properties
browser/components/newtab/locales-src/ru/strings.properties
browser/components/newtab/locales-src/si/strings.properties
browser/components/newtab/locales-src/sk/strings.properties
browser/components/newtab/locales-src/sl/strings.properties
browser/components/newtab/locales-src/sq/strings.properties
browser/components/newtab/locales-src/sr/strings.properties
browser/components/newtab/locales-src/sv-SE/strings.properties
browser/components/newtab/locales-src/ta/strings.properties
browser/components/newtab/locales-src/te/strings.properties
browser/components/newtab/locales-src/th/strings.properties
browser/components/newtab/locales-src/tl/strings.properties
browser/components/newtab/locales-src/tr/strings.properties
browser/components/newtab/locales-src/trs/strings.properties
browser/components/newtab/locales-src/uk/strings.properties
browser/components/newtab/locales-src/ur/strings.properties
browser/components/newtab/locales-src/uz/strings.properties
browser/components/newtab/locales-src/vi/strings.properties
browser/components/newtab/locales-src/zh-CN/strings.properties
browser/components/newtab/locales-src/zh-TW/strings.properties
browser/components/newtab/prerendered/locales/ach/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ach/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ach/activity-stream.html
browser/components/newtab/prerendered/locales/an/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/an/activity-stream-strings.js
browser/components/newtab/prerendered/locales/an/activity-stream.html
browser/components/newtab/prerendered/locales/ar/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ar/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ar/activity-stream.html
browser/components/newtab/prerendered/locales/ast/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ast/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ast/activity-stream.html
browser/components/newtab/prerendered/locales/az/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/az/activity-stream-strings.js
browser/components/newtab/prerendered/locales/az/activity-stream.html
browser/components/newtab/prerendered/locales/be/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/be/activity-stream-strings.js
browser/components/newtab/prerendered/locales/be/activity-stream.html
browser/components/newtab/prerendered/locales/bg/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/bg/activity-stream-strings.js
browser/components/newtab/prerendered/locales/bg/activity-stream.html
browser/components/newtab/prerendered/locales/bn/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/bn/activity-stream-strings.js
browser/components/newtab/prerendered/locales/bn/activity-stream.html
browser/components/newtab/prerendered/locales/br/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/br/activity-stream-strings.js
browser/components/newtab/prerendered/locales/br/activity-stream.html
browser/components/newtab/prerendered/locales/bs/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/bs/activity-stream-strings.js
browser/components/newtab/prerendered/locales/bs/activity-stream.html
browser/components/newtab/prerendered/locales/ca/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ca/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ca/activity-stream.html
browser/components/newtab/prerendered/locales/cak/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/cak/activity-stream-strings.js
browser/components/newtab/prerendered/locales/cak/activity-stream.html
browser/components/newtab/prerendered/locales/crh/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/crh/activity-stream-strings.js
browser/components/newtab/prerendered/locales/crh/activity-stream.html
browser/components/newtab/prerendered/locales/cs/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/cs/activity-stream-strings.js
browser/components/newtab/prerendered/locales/cs/activity-stream.html
browser/components/newtab/prerendered/locales/cy/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/cy/activity-stream-strings.js
browser/components/newtab/prerendered/locales/cy/activity-stream.html
browser/components/newtab/prerendered/locales/da/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/da/activity-stream-strings.js
browser/components/newtab/prerendered/locales/da/activity-stream.html
browser/components/newtab/prerendered/locales/de/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/de/activity-stream-strings.js
browser/components/newtab/prerendered/locales/de/activity-stream.html
browser/components/newtab/prerendered/locales/dsb/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/dsb/activity-stream-strings.js
browser/components/newtab/prerendered/locales/dsb/activity-stream.html
browser/components/newtab/prerendered/locales/el/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/el/activity-stream-strings.js
browser/components/newtab/prerendered/locales/el/activity-stream.html
browser/components/newtab/prerendered/locales/en-CA/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/en-CA/activity-stream-strings.js
browser/components/newtab/prerendered/locales/en-CA/activity-stream.html
browser/components/newtab/prerendered/locales/en-GB/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/en-GB/activity-stream-strings.js
browser/components/newtab/prerendered/locales/en-GB/activity-stream.html
browser/components/newtab/prerendered/locales/en-US/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/en-US/activity-stream-strings.js
browser/components/newtab/prerendered/locales/en-US/activity-stream.html
browser/components/newtab/prerendered/locales/eo/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/eo/activity-stream-strings.js
browser/components/newtab/prerendered/locales/eo/activity-stream.html
browser/components/newtab/prerendered/locales/es-AR/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/es-AR/activity-stream-strings.js
browser/components/newtab/prerendered/locales/es-AR/activity-stream.html
browser/components/newtab/prerendered/locales/es-CL/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/es-CL/activity-stream-strings.js
browser/components/newtab/prerendered/locales/es-CL/activity-stream.html
browser/components/newtab/prerendered/locales/es-ES/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/es-ES/activity-stream-strings.js
browser/components/newtab/prerendered/locales/es-ES/activity-stream.html
browser/components/newtab/prerendered/locales/es-MX/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/es-MX/activity-stream-strings.js
browser/components/newtab/prerendered/locales/es-MX/activity-stream.html
browser/components/newtab/prerendered/locales/et/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/et/activity-stream-strings.js
browser/components/newtab/prerendered/locales/et/activity-stream.html
browser/components/newtab/prerendered/locales/eu/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/eu/activity-stream-strings.js
browser/components/newtab/prerendered/locales/eu/activity-stream.html
browser/components/newtab/prerendered/locales/fa/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/fa/activity-stream-strings.js
browser/components/newtab/prerendered/locales/fa/activity-stream.html
browser/components/newtab/prerendered/locales/ff/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ff/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ff/activity-stream.html
browser/components/newtab/prerendered/locales/fi/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/fi/activity-stream-strings.js
browser/components/newtab/prerendered/locales/fi/activity-stream.html
browser/components/newtab/prerendered/locales/fr/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/fr/activity-stream-strings.js
browser/components/newtab/prerendered/locales/fr/activity-stream.html
browser/components/newtab/prerendered/locales/fy-NL/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/fy-NL/activity-stream-strings.js
browser/components/newtab/prerendered/locales/fy-NL/activity-stream.html
browser/components/newtab/prerendered/locales/ga-IE/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ga-IE/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ga-IE/activity-stream.html
browser/components/newtab/prerendered/locales/gd/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/gd/activity-stream-strings.js
browser/components/newtab/prerendered/locales/gd/activity-stream.html
browser/components/newtab/prerendered/locales/gl/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/gl/activity-stream-strings.js
browser/components/newtab/prerendered/locales/gl/activity-stream.html
browser/components/newtab/prerendered/locales/gn/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/gn/activity-stream-strings.js
browser/components/newtab/prerendered/locales/gn/activity-stream.html
browser/components/newtab/prerendered/locales/gu-IN/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/gu-IN/activity-stream-strings.js
browser/components/newtab/prerendered/locales/gu-IN/activity-stream.html
browser/components/newtab/prerendered/locales/he/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/he/activity-stream-strings.js
browser/components/newtab/prerendered/locales/he/activity-stream.html
browser/components/newtab/prerendered/locales/hi-IN/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/hi-IN/activity-stream-strings.js
browser/components/newtab/prerendered/locales/hi-IN/activity-stream.html
browser/components/newtab/prerendered/locales/hr/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/hr/activity-stream-strings.js
browser/components/newtab/prerendered/locales/hr/activity-stream.html
browser/components/newtab/prerendered/locales/hsb/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/hsb/activity-stream-strings.js
browser/components/newtab/prerendered/locales/hsb/activity-stream.html
browser/components/newtab/prerendered/locales/hu/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/hu/activity-stream-strings.js
browser/components/newtab/prerendered/locales/hu/activity-stream.html
browser/components/newtab/prerendered/locales/hy-AM/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/hy-AM/activity-stream-strings.js
browser/components/newtab/prerendered/locales/hy-AM/activity-stream.html
browser/components/newtab/prerendered/locales/ia/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ia/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ia/activity-stream.html
browser/components/newtab/prerendered/locales/id/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/id/activity-stream-strings.js
browser/components/newtab/prerendered/locales/id/activity-stream.html
browser/components/newtab/prerendered/locales/is/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/is/activity-stream-strings.js
browser/components/newtab/prerendered/locales/is/activity-stream.html
browser/components/newtab/prerendered/locales/it/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/it/activity-stream-strings.js
browser/components/newtab/prerendered/locales/it/activity-stream.html
browser/components/newtab/prerendered/locales/ja/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ja/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ja/activity-stream.html
browser/components/newtab/prerendered/locales/ka/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ka/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ka/activity-stream.html
browser/components/newtab/prerendered/locales/kab/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/kab/activity-stream-strings.js
browser/components/newtab/prerendered/locales/kab/activity-stream.html
browser/components/newtab/prerendered/locales/kk/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/kk/activity-stream-strings.js
browser/components/newtab/prerendered/locales/kk/activity-stream.html
browser/components/newtab/prerendered/locales/km/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/km/activity-stream-strings.js
browser/components/newtab/prerendered/locales/km/activity-stream.html
browser/components/newtab/prerendered/locales/kn/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/kn/activity-stream-strings.js
browser/components/newtab/prerendered/locales/kn/activity-stream.html
browser/components/newtab/prerendered/locales/ko/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ko/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ko/activity-stream.html
browser/components/newtab/prerendered/locales/lij/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/lij/activity-stream-strings.js
browser/components/newtab/prerendered/locales/lij/activity-stream.html
browser/components/newtab/prerendered/locales/lo/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/lo/activity-stream-strings.js
browser/components/newtab/prerendered/locales/lo/activity-stream.html
browser/components/newtab/prerendered/locales/lt/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/lt/activity-stream-strings.js
browser/components/newtab/prerendered/locales/lt/activity-stream.html
browser/components/newtab/prerendered/locales/ltg/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ltg/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ltg/activity-stream.html
browser/components/newtab/prerendered/locales/lv/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/lv/activity-stream-strings.js
browser/components/newtab/prerendered/locales/lv/activity-stream.html
browser/components/newtab/prerendered/locales/mk/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/mk/activity-stream-strings.js
browser/components/newtab/prerendered/locales/mk/activity-stream.html
browser/components/newtab/prerendered/locales/mr/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/mr/activity-stream-strings.js
browser/components/newtab/prerendered/locales/mr/activity-stream.html
browser/components/newtab/prerendered/locales/ms/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ms/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ms/activity-stream.html
browser/components/newtab/prerendered/locales/my/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/my/activity-stream-strings.js
browser/components/newtab/prerendered/locales/my/activity-stream.html
browser/components/newtab/prerendered/locales/nb-NO/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/nb-NO/activity-stream-strings.js
browser/components/newtab/prerendered/locales/nb-NO/activity-stream.html
browser/components/newtab/prerendered/locales/ne-NP/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ne-NP/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ne-NP/activity-stream.html
browser/components/newtab/prerendered/locales/nl/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/nl/activity-stream-strings.js
browser/components/newtab/prerendered/locales/nl/activity-stream.html
browser/components/newtab/prerendered/locales/nn-NO/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/nn-NO/activity-stream-strings.js
browser/components/newtab/prerendered/locales/nn-NO/activity-stream.html
browser/components/newtab/prerendered/locales/oc/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/oc/activity-stream-strings.js
browser/components/newtab/prerendered/locales/oc/activity-stream.html
browser/components/newtab/prerendered/locales/pa-IN/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/pa-IN/activity-stream-strings.js
browser/components/newtab/prerendered/locales/pa-IN/activity-stream.html
browser/components/newtab/prerendered/locales/pl/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/pl/activity-stream-strings.js
browser/components/newtab/prerendered/locales/pl/activity-stream.html
browser/components/newtab/prerendered/locales/pt-BR/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/pt-BR/activity-stream-strings.js
browser/components/newtab/prerendered/locales/pt-BR/activity-stream.html
browser/components/newtab/prerendered/locales/pt-PT/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/pt-PT/activity-stream-strings.js
browser/components/newtab/prerendered/locales/pt-PT/activity-stream.html
browser/components/newtab/prerendered/locales/rm/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/rm/activity-stream-strings.js
browser/components/newtab/prerendered/locales/rm/activity-stream.html
browser/components/newtab/prerendered/locales/ro/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ro/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ro/activity-stream.html
browser/components/newtab/prerendered/locales/ru/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ru/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ru/activity-stream.html
browser/components/newtab/prerendered/locales/si/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/si/activity-stream-strings.js
browser/components/newtab/prerendered/locales/si/activity-stream.html
browser/components/newtab/prerendered/locales/sk/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/sk/activity-stream-strings.js
browser/components/newtab/prerendered/locales/sk/activity-stream.html
browser/components/newtab/prerendered/locales/sl/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/sl/activity-stream-strings.js
browser/components/newtab/prerendered/locales/sl/activity-stream.html
browser/components/newtab/prerendered/locales/sq/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/sq/activity-stream-strings.js
browser/components/newtab/prerendered/locales/sq/activity-stream.html
browser/components/newtab/prerendered/locales/sr/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/sr/activity-stream-strings.js
browser/components/newtab/prerendered/locales/sr/activity-stream.html
browser/components/newtab/prerendered/locales/sv-SE/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/sv-SE/activity-stream-strings.js
browser/components/newtab/prerendered/locales/sv-SE/activity-stream.html
browser/components/newtab/prerendered/locales/ta/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ta/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ta/activity-stream.html
browser/components/newtab/prerendered/locales/te/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/te/activity-stream-strings.js
browser/components/newtab/prerendered/locales/te/activity-stream.html
browser/components/newtab/prerendered/locales/th/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/th/activity-stream-strings.js
browser/components/newtab/prerendered/locales/th/activity-stream.html
browser/components/newtab/prerendered/locales/tl/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/tl/activity-stream-strings.js
browser/components/newtab/prerendered/locales/tl/activity-stream.html
browser/components/newtab/prerendered/locales/tr/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/tr/activity-stream-strings.js
browser/components/newtab/prerendered/locales/tr/activity-stream.html
browser/components/newtab/prerendered/locales/trs/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/trs/activity-stream-strings.js
browser/components/newtab/prerendered/locales/trs/activity-stream.html
browser/components/newtab/prerendered/locales/uk/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/uk/activity-stream-strings.js
browser/components/newtab/prerendered/locales/uk/activity-stream.html
browser/components/newtab/prerendered/locales/ur/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/ur/activity-stream-strings.js
browser/components/newtab/prerendered/locales/ur/activity-stream.html
browser/components/newtab/prerendered/locales/uz/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/uz/activity-stream-strings.js
browser/components/newtab/prerendered/locales/uz/activity-stream.html
browser/components/newtab/prerendered/locales/vi/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/vi/activity-stream-strings.js
browser/components/newtab/prerendered/locales/vi/activity-stream.html
browser/components/newtab/prerendered/locales/zh-CN/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/zh-CN/activity-stream-strings.js
browser/components/newtab/prerendered/locales/zh-CN/activity-stream.html
browser/components/newtab/prerendered/locales/zh-TW/activity-stream-noscripts.html
browser/components/newtab/prerendered/locales/zh-TW/activity-stream-strings.js
browser/components/newtab/prerendered/locales/zh-TW/activity-stream.html
browser/components/newtab/prerendered/static/activity-stream-debug.html
browser/components/newtab/test/browser/browser_activity_stream_strings.js
browser/components/newtab/test/browser/browser_packaged_as_locales.js
browser/components/newtab/vendor/REACT_INTL_LICENSE
browser/components/newtab/vendor/react-intl.js
browser/locales/en-US/chrome/browser/activity-stream/newtab.properties
devtools/client/themes/images/webconsole/error.svg
devtools/client/themes/images/webconsole/info.svg
dom/base/WebKitCSSMatrix.cpp
dom/base/WebKitCSSMatrix.h
dom/clients/manager/ClientPrefs.cpp
dom/clients/manager/ClientPrefs.h
dom/webidl/WebKitCSSMatrix.webidl
js/src/jit-test/tests/basic/perf-smoketest.js
js/src/perf/jsperf.cpp
js/src/perf/jsperf.h
js/src/perf/pm_linux.cpp
js/src/perf/pm_stub.cpp
js/src/wasm/cranelift/src/baldrapi.rs
js/src/wasm/cranelift/src/baldrdash.rs
js/src/wasm/cranelift/src/cpu.rs
other-licenses/nsis/Contrib/InetBgDL/InetBgDL.dsp
other-licenses/nsis/Contrib/InetBgDL/InetBgDL.dsw
python/l10n/fluent_migrations/bug_1495861_panicButton.py
python/l10n/fluent_migrations/bug_1517528_aboutPrivateBrowsing.py
python/l10n/fluent_migrations/bug_1521800_passwordManager.py
python/l10n/fluent_migrations/bug_1523734_aboutBlocked.py
python/l10n/fluent_migrations/bug_1523741_aboutTelemetry.py
python/l10n/fluent_migrations/bug_1523747_customizeMode.py
python/l10n/fluent_migrations/bug_1523757_panelUI.py
python/l10n/fluent_migrations/bug_1523761_syncedTabs.py
python/l10n/fluent_migrations/bug_1523763_tabContextMenu.py
python/l10n/fluent_migrations/bug_1529071_printPreview.py
python/l10n/fluent_migrations/bug_1544501_toolbarContextMenu.py
taskcluster/ci/build/linux.yml
taskcluster/scripts/misc/build-cctools-port-macosx.sh
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-binast-instagram.manifest
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-ebay.manifest
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-linkedin.manifest
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-office.manifest
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-paypal.manifest
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-pinterest.manifest
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-tp6-1.manifest
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-tp6-2.manifest
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-tp6-3.manifest
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-tp6-4.manifest
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-tp6-6.manifest
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-tp6-7.manifest
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-tumblr.manifest
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-twitter.manifest
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-wikipedia.manifest
testing/raptor/raptor/playback/mitmproxy-recordings-raptor-yahoo-news.manifest
testing/web-platform/meta/css/geometry/DOMMatrix-003.html.ini
testing/web-platform/meta/css/geometry/DOMMatrix-newobject.html.ini
testing/web-platform/meta/css/geometry/DOMQuad-002.html.ini
testing/web-platform/meta/css/geometry/DOMRect-002.html.ini
testing/web-platform/meta/css/geometry/WebKitCSSMatrix.html.ini
testing/web-platform/meta/mediacapture-record/MediaRecorder-disabled-tracks.https.html.ini
testing/web-platform/meta/url/idlharness.any.js.ini
testing/web-platform/meta/webmessaging/with-ports/016.html.ini
testing/web-platform/meta/webmessaging/without-ports/016.html.ini
testing/web-platform/tests/css/css-text-decor/reference/text-decoration-width-linethrough-001-ref.html
toolkit/components/perf/.eslintrc.js
toolkit/components/perf/PerfMeasurement.cpp
toolkit/components/perf/PerfMeasurement.h
toolkit/components/perf/PerfMeasurement.jsm
toolkit/components/perf/chrome.ini
toolkit/components/perf/components.conf
toolkit/components/perf/moz.build
toolkit/components/perf/test_pm.xul
toolkit/modules/SelectParentHelper.jsm
--- a/.cargo/config.in
+++ b/.cargo/config.in
@@ -19,13 +19,13 @@ replace-with = "vendored-sources"
 
 [source."https://github.com/rust-lang-nursery/packed_simd"]
 git = "https://github.com/hsivonen/packed_simd"
 branch = "rust_1_32"
 replace-with = "vendored-sources"
 
 [source."https://github.com/CraneStation/Cranelift"]
 git = "https://github.com/CraneStation/Cranelift"
-rev = "475aa632fea5360c6f8c4cc1f26e3ee0369385ef"
+rev = "e455f6ae0f3577ceb210c0ce167181c33c133a69"
 replace-with = "vendored-sources"
 
 [source.vendored-sources]
 directory = '@top_srcdir@/third_party/rust'
--- a/.clang-format-ignore
+++ b/.clang-format-ignore
@@ -113,17 +113,17 @@ js/src/vtune/ittnotify_types.h
 js/src/vtune/jitprofiling.c
 js/src/vtune/jitprofiling.h
 js/src/vtune/legacy/.*
 media/ffvpx/.*
 media/gmp-clearkey/0.1/openaes/.*
 media/kiss_fft/.*
 media/libaom/.*
 media/libcubeb/.*
-media/libdav1d/version.h
+media/libdav1d/.*
 media/libjpeg/.*
 media/libmkv/.*
 media/libnestegg/.*
 media/libogg/.*
 media/libopus/.*
 media/libpng/.*
 media/libsoundtouch/.*
 media/libspeex_resampler/.*
--- a/.cron.yml
+++ b/.cron.yml
@@ -171,16 +171,19 @@ jobs:
                   - {weekday: 'Monday', hour: 10, minute: 0}
                   - {weekday: 'Thursday', hour: 10, minute: 0}
               mozilla-release:
                   - {weekday: 'Monday', hour: 10, minute: 0}
                   - {weekday: 'Thursday', hour: 10, minute: 0}
               mozilla-esr60:
                   - {weekday: 'Monday', hour: 10, minute: 0}
                   - {weekday: 'Thursday', hour: 10, minute: 0}
+              mozilla-esr68:
+                  - {weekday: 'Monday', hour: 10, minute: 0}
+                  - {weekday: 'Thursday', hour: 10, minute: 0}
 
     - name: pipfile-update
       job:
           type: decision-task
           treeherder-symbol: Nfile
           target-tasks-method: pipfile_update
       run-on-projects:
           - mozilla-central
--- a/.eslintignore
+++ b/.eslintignore
@@ -45,17 +45,16 @@ browser/extensions/pdfjs/content/web**
 # generated or library files in pocket
 browser/components/pocket/content/panels/js/tmpl.js
 browser/components/pocket/content/panels/js/vendor/**
 
 # Ignore newtab files
 # Kept in sync with browser/components/newtab/.eslintignore
 browser/components/newtab/data/
 browser/components/newtab/logs/
-browser/components/newtab/prerendered/
 browser/components/newtab/vendor/
 
 # The only file in browser/locales/ is pre-processed.
 browser/locales/**
 # imported from chromium
 browser/extensions/mortar/**
 # Generated data files
 browser/extensions/formautofill/phonenumberutils/PhoneNumberMetaData.jsm
--- a/.gitignore
+++ b/.gitignore
@@ -39,19 +39,16 @@ security/manager/.nss.checkout
 
 # Build directories
 /obj*/
 
 # gecko.log is generated by various test harnesses
 /gecko.log
 
 # Ignore newtab component build assets
-browser/components/newtab/bin/prerender.js
-browser/components/newtab/bin/prerender.js.map
-browser/components/newtab/data/locales.json
 browser/components/newtab/logs/
 
 # Build directories for js shell
 *_DBG.OBJ/
 *_OPT.OBJ/
 /js/src/*-obj/
 /js/src/obj-*/
 
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -192,18 +192,18 @@ dependencies = [
  "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "baldrdash"
 version = "0.1.0"
 dependencies = [
  "bindgen 0.49.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "cranelift-codegen 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)",
- "cranelift-wasm 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)",
+ "cranelift-codegen 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)",
+ "cranelift-wasm 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)",
  "env_logger 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "target-lexicon 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "base64"
 version = "0.9.3"
@@ -608,70 +608,70 @@ version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "cose 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "cranelift-bforest"
 version = "0.30.0"
-source = "git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef#475aa632fea5360c6f8c4cc1f26e3ee0369385ef"
-dependencies = [
- "cranelift-entity 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)",
+source = "git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69#e455f6ae0f3577ceb210c0ce167181c33c133a69"
+dependencies = [
+ "cranelift-entity 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)",
 ]
 
 [[package]]
 name = "cranelift-codegen"
 version = "0.30.0"
-source = "git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef#475aa632fea5360c6f8c4cc1f26e3ee0369385ef"
-dependencies = [
- "cranelift-bforest 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)",
- "cranelift-codegen-meta 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)",
- "cranelift-entity 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)",
+source = "git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69#e455f6ae0f3577ceb210c0ce167181c33c133a69"
+dependencies = [
+ "cranelift-bforest 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)",
+ "cranelift-codegen-meta 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)",
+ "cranelift-entity 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)",
  "failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "failure_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "target-lexicon 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "cranelift-codegen-meta"
 version = "0.30.0"
-source = "git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef#475aa632fea5360c6f8c4cc1f26e3ee0369385ef"
-dependencies = [
- "cranelift-entity 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)",
+source = "git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69#e455f6ae0f3577ceb210c0ce167181c33c133a69"
+dependencies = [
+ "cranelift-entity 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)",
 ]
 
 [[package]]
 name = "cranelift-entity"
 version = "0.30.0"
-source = "git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef#475aa632fea5360c6f8c4cc1f26e3ee0369385ef"
+source = "git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69#e455f6ae0f3577ceb210c0ce167181c33c133a69"
 
 [[package]]
 name = "cranelift-frontend"
 version = "0.30.0"
-source = "git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef#475aa632fea5360c6f8c4cc1f26e3ee0369385ef"
-dependencies = [
- "cranelift-codegen 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)",
+source = "git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69#e455f6ae0f3577ceb210c0ce167181c33c133a69"
+dependencies = [
+ "cranelift-codegen 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)",
  "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "target-lexicon 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "cranelift-wasm"
 version = "0.30.0"
-source = "git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef#475aa632fea5360c6f8c4cc1f26e3ee0369385ef"
-dependencies = [
- "cranelift-codegen 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)",
- "cranelift-entity 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)",
- "cranelift-frontend 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)",
+source = "git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69#e455f6ae0f3577ceb210c0ce167181c33c133a69"
+dependencies = [
+ "cranelift-codegen 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)",
+ "cranelift-entity 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)",
+ "cranelift-frontend 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)",
  "failure 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "failure_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "wasmparser 0.29.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "wasmparser 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "crc"
 version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "build_const 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -3535,17 +3535,17 @@ dependencies = [
  "tokio-io 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio-threadpool 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
  "tungstenite 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "urlencoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "wasmparser"
-version = "0.29.2"
+version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "webdriver"
 version = "0.39.0"
 dependencies = [
  "base64 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "cookie 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -3882,22 +3882,22 @@ dependencies = [
 "checksum constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8ff012e225ce166d4422e0e78419d901719760f62ae2b7969ca6b564d1b54a9e"
 "checksum cookie 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1465f8134efa296b4c19db34d909637cb2bf0f7aaf21299e23e18fa29ac557cf"
 "checksum core-foundation 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4e2640d6d0bf22e82bed1b73c6aef8d5dd31e5abe6666c57e6d45e2649f4f887"
 "checksum core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
 "checksum core-graphics 0.17.1 (registry+https://github.com/rust-lang/crates.io-index)" = "62ceafe1622ffc9a332199096841d0ff9912ec8cf8f9cde01e254a7d5217cd10"
 "checksum core-text 13.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f3f46450d6f2397261af420b4ccce23807add2e45fa206410a03d66fb7f050ae"
 "checksum cose 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "72fa26cb151d3ae4b70f63d67d0fed57ce04220feafafbae7f503bef7aae590d"
 "checksum cose-c 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "49726015ab0ca765144fcca61e4a7a543a16b795a777fa53f554da2fffff9a94"
-"checksum cranelift-bforest 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)" = "<none>"
-"checksum cranelift-codegen 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)" = "<none>"
-"checksum cranelift-codegen-meta 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)" = "<none>"
-"checksum cranelift-entity 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)" = "<none>"
-"checksum cranelift-frontend 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)" = "<none>"
-"checksum cranelift-wasm 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=475aa632fea5360c6f8c4cc1f26e3ee0369385ef)" = "<none>"
+"checksum cranelift-bforest 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)" = "<none>"
+"checksum cranelift-codegen 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)" = "<none>"
+"checksum cranelift-codegen-meta 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)" = "<none>"
+"checksum cranelift-entity 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)" = "<none>"
+"checksum cranelift-frontend 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)" = "<none>"
+"checksum cranelift-wasm 0.30.0 (git+https://github.com/CraneStation/Cranelift?rev=e455f6ae0f3577ceb210c0ce167181c33c133a69)" = "<none>"
 "checksum crc 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bd5d02c0aac6bd68393ed69e00bbc2457f3e89075c6349db7189618dc4ddc1d7"
 "checksum crossbeam-channel 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "8d4f5844607ce8da3fff431e7dba56cda8bfcc570aa50bee36adba8a32b8cad7"
 "checksum crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f739f8c5363aca78cfb059edf753d8f0d36908c348f3d8d1503f03d8b75d9cf3"
 "checksum crossbeam-deque 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "05e44b8cf3e1a625844d1750e1f7820da46044ff6d28f4d43e455ba3e5bb2c13"
 "checksum crossbeam-epoch 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "927121f5407de9956180ff5e936fe3cf4324279280001cd56b669d28ee7e9150"
 "checksum crossbeam-epoch 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "04c9e3102cc2d69cd681412141b390abd55a362afc1540965dad0ad4d34280b4"
 "checksum crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2760899e32a1d58d5abb31129f8fae5de75220bc2176e77ff7c627ae45c918d9"
 "checksum crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f8306fcef4a7b563b76b7dd949ca48f52bc1141aa067d2ea09565f3e2652aa5c"
@@ -4153,17 +4153,17 @@ dependencies = [
 "checksum uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a"
 "checksum vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9e0a7d8bed3178a8fb112199d466eeca9ed09a14ba8ad67718179b4fd5487d0b"
 "checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c"
 "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
 "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
 "checksum walkdir 2.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "63636bd0eb3d00ccb8b9036381b526efac53caf112b7783b730ab3f8e44da369"
 "checksum want 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "797464475f30ddb8830cc529aaaae648d581f99e2036a928877dfde027ddf6b3"
 "checksum warp 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "8f0a120bf7041d4381a5429c4e6d12633bfb874c968a78ec3a3563e9ca6e12d6"
-"checksum wasmparser 0.29.2 (registry+https://github.com/rust-lang/crates.io-index)" = "981a8797cf89762e0233ec45fae731cb79a4dfaee12d9f0fe6cee01e4ac58d00"
+"checksum wasmparser 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8a6f324afc05fd8282bbc49dae854a1c20f74aeff10a575b5a43453d1864db97"
 "checksum weedle 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "26a4c67f132386d965390b8a734d5d10adbcd30eb5cc74bd9229af8b83f10044"
 "checksum which 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4be6cfa54dab45266e98b5d7be2f8ce959ddd49abd141a05d52dce4b07f803bb"
 "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
 "checksum winapi 0.3.6 (git+https://github.com/froydnj/winapi-rs?branch=aarch64)" = "<none>"
 "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
 "checksum winapi-i686-pc-windows-gnu 0.4.0 (git+https://github.com/froydnj/winapi-rs?branch=aarch64)" = "<none>"
 "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (git+https://github.com/froydnj/winapi-rs?branch=aarch64)" = "<none>"
 "checksum wincolor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eeb06499a3a4d44302791052df005d5232b927ed1a9658146d842165c4de7767"
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -59,13 +59,13 @@ codegen-units = 1
 [patch.crates-io]
 libudev-sys = { path = "dom/webauthn/libudev-sys" }
 serde_derive = { git = "https://github.com/servo/serde", branch = "deserialize_from_enums10" }
 winapi = { git = "https://github.com/froydnj/winapi-rs", branch = "aarch64" }
 packed_simd = { git = "https://github.com/hsivonen/packed_simd", branch = "rust_1_32" }
 
 [patch.crates-io.cranelift-codegen]
 git = "https://github.com/CraneStation/Cranelift"
-rev = "475aa632fea5360c6f8c4cc1f26e3ee0369385ef"
+rev = "e455f6ae0f3577ceb210c0ce167181c33c133a69"
 
 [patch.crates-io.cranelift-wasm]
 git = "https://github.com/CraneStation/Cranelift"
-rev = "475aa632fea5360c6f8c4cc1f26e3ee0369385ef"
+rev = "e455f6ae0f3577ceb210c0ce167181c33c133a69"
--- a/accessible/base/nsCoreUtils.cpp
+++ b/accessible/base/nsCoreUtils.cpp
@@ -229,17 +229,17 @@ nsresult nsCoreUtils::ScrollSubstringTo(
   nsCOMPtr<nsISelectionController> selCon;
   aFrame->GetSelectionController(presContext, getter_AddRefs(selCon));
   NS_ENSURE_TRUE(selCon, NS_ERROR_FAILURE);
 
   RefPtr<dom::Selection> selection =
       selCon->GetSelection(nsISelectionController::SELECTION_ACCESSIBILITY);
 
   selection->RemoveAllRanges(IgnoreErrors());
-  selection->AddRange(*aRange, IgnoreErrors());
+  selection->AddRangeAndSelectFramesAndNotifyListeners(*aRange, IgnoreErrors());
 
   selection->ScrollIntoView(nsISelectionController::SELECTION_ANCHOR_REGION,
                             aVertical, aHorizontal,
                             Selection::SCROLL_SYNCHRONOUS);
 
   selection->CollapseToStart(IgnoreErrors());
 
   return NS_OK;
--- a/accessible/generic/Accessible.cpp
+++ b/accessible/generic/Accessible.cpp
@@ -33,16 +33,17 @@
 
 #include "nsIDOMXULButtonElement.h"
 #include "nsIDOMXULSelectCntrlEl.h"
 #include "nsIDOMXULSelectCntrlItemEl.h"
 #include "nsINodeList.h"
 #include "nsPIDOMWindow.h"
 
 #include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLFormElement.h"
 #include "nsIContent.h"
 #include "nsIForm.h"
 #include "nsIFormControl.h"
 
 #include "nsDeckFrame.h"
 #include "nsLayoutUtils.h"
 #include "nsIStringBundle.h"
 #include "nsPresContext.h"
@@ -1725,18 +1726,17 @@ Relation Accessible::RelationByType(Rela
     case RelationType::PARENT_WINDOW_OF:
       return Relation();
 
     case RelationType::DEFAULT_BUTTON: {
       if (mContent->IsHTMLElement()) {
         // HTML form controls implements nsIFormControl interface.
         nsCOMPtr<nsIFormControl> control(do_QueryInterface(mContent));
         if (control) {
-          nsCOMPtr<nsIForm> form(do_QueryInterface(control->GetFormElement()));
-          if (form) {
+          if (dom::HTMLFormElement* form = control->GetFormElement()) {
             nsCOMPtr<nsIContent> formContent =
                 do_QueryInterface(form->GetDefaultSubmitElement());
             return Relation(mDoc, formContent);
           }
         }
       } else {
         // In XUL, use first <button default="true" .../> in the document
         dom::Document* doc = mContent->OwnerDoc();
--- a/accessible/generic/HyperTextAccessible.cpp
+++ b/accessible/generic/HyperTextAccessible.cpp
@@ -1267,17 +1267,18 @@ nsresult HyperTextAccessible::SetSelecti
   // some input controls
   if (isFocusable) TakeFocus();
 
   dom::Selection* domSel = DOMSelection();
   NS_ENSURE_STATE(domSel);
 
   // Set up the selection.
   for (int32_t idx = domSel->RangeCount() - 1; idx > 0; idx--)
-    domSel->RemoveRange(*domSel->GetRangeAt(idx), IgnoreErrors());
+    domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(
+        *domSel->GetRangeAt(idx), IgnoreErrors());
   SetSelectionBoundsAt(0, aStartPos, aEndPos);
 
   // Make sure it is visible
   domSel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
                          ScrollAxis(), ScrollAxis(),
                          dom::Selection::SCROLL_FOR_CARET_MOVE |
                              dom::Selection::SCROLL_OVERFLOW_HIDDEN);
 
@@ -1566,21 +1567,22 @@ bool HyperTextAccessible::SetSelectionBo
 
   if (!OffsetsToDOMRange(std::min(startOffset, endOffset),
                          std::max(startOffset, endOffset), range))
     return false;
 
   // If this is not a new range, notify selection listeners that the existing
   // selection range has changed. Otherwise, just add the new range.
   if (aSelectionNum != static_cast<int32_t>(rangeCount)) {
-    domSel->RemoveRange(*range, IgnoreErrors());
+    domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
+                                                           IgnoreErrors());
   }
 
   IgnoredErrorResult err;
-  domSel->AddRange(*range, err);
+  domSel->AddRangeAndSelectFramesAndNotifyListeners(*range, err);
 
   if (!err.Failed()) {
     // Changing the direction of the selection assures that the caret
     // will be at the logical end of the selection.
     domSel->SetDirection(startOffset < endOffset ? eDirNext : eDirPrevious);
     return true;
   }
 
@@ -1590,17 +1592,18 @@ bool HyperTextAccessible::SetSelectionBo
 bool HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum) {
   dom::Selection* domSel = DOMSelection();
   if (!domSel) return false;
 
   if (aSelectionNum < 0 ||
       aSelectionNum >= static_cast<int32_t>(domSel->RangeCount()))
     return false;
 
-  domSel->RemoveRange(*domSel->GetRangeAt(aSelectionNum), IgnoreErrors());
+  domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(
+      *domSel->GetRangeAt(aSelectionNum), IgnoreErrors());
   return true;
 }
 
 void HyperTextAccessible::ScrollSubstringTo(int32_t aStartOffset,
                                             int32_t aEndOffset,
                                             uint32_t aScrollType) {
   RefPtr<nsRange> range = new nsRange(mContent);
   if (OffsetsToDOMRange(aStartOffset, aEndOffset, range))
--- a/accessible/ipc/DocAccessibleParent.cpp
+++ b/accessible/ipc/DocAccessibleParent.cpp
@@ -841,22 +841,29 @@ mozilla::ipc::IPCResult DocAccessiblePar
 #endif  // defined(XP_WIN)
 
 #if !defined(XP_WIN)
 mozilla::ipc::IPCResult DocAccessibleParent::RecvBatch(
     const uint64_t& aBatchType, nsTArray<BatchData>&& aData) {
   // Only do something in Android. We can't ifdef the entire protocol out in
   // the ipdl because it doesn't allow preprocessing.
 #  if defined(ANDROID)
+  if (mShutdown) {
+    return IPC_OK();
+  }
   nsTArray<ProxyAccessible*> proxies(aData.Length());
   for (size_t i = 0; i < aData.Length(); i++) {
     DocAccessibleParent* doc = static_cast<DocAccessibleParent*>(
         aData.ElementAt(i).Document().get_PDocAccessibleParent());
     MOZ_ASSERT(doc);
 
+    if (doc->IsShutdown()) {
+      continue;
+    }
+
     ProxyAccessible* proxy = doc->GetAccessible(aData.ElementAt(i).ID());
     if (!proxy) {
       MOZ_ASSERT_UNREACHABLE("No proxy found!");
       continue;
     }
 
     proxies.AppendElement(proxy);
   }
--- a/accessible/tests/mochitest/name/test_list.html
+++ b/accessible/tests/mochitest/name/test_list.html
@@ -34,17 +34,23 @@
 
       this.finalCheck = function bulletUpdate_finalCheck() {
         testName("li_start", "1. list start");
         testName("li_end", "2. list end");
 
         // change list style type
         var list = getNode("list");
         list.setAttribute("style", "list-style-type: disc;");
-        getComputedStyle(list, "").color; // make style processing sync
+
+        // Flush both the style change and the resulting layout change.
+        // Flushing style on its own is not sufficient, because that can
+        // leave frames marked with NS_FRAME_IS_DIRTY, which will cause
+        // nsTextFrame::GetRenderedText to report the text of a text
+        // frame is empty.
+        list.offsetWidth; // flush layout (which also flushes style)
 
         testName("li_start", kDiscBulletText + "list start");
         testName("li_end", kDiscBulletText + "list end");
       };
 
       this.getID = function bulletUpdate_getID() {
         return "Update bullet of list items";
       };
--- a/accessible/xpcom/AccEventGen.py
+++ b/accessible/xpcom/AccEventGen.py
@@ -1,14 +1,15 @@
 #!/usr/bin/env python
 #
 # 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/.
 
+from __future__ import absolute_import
 import sys
 import os
 
 import buildconfig
 import mozpack.path as mozpath
 
 # The xpidl parser is not incorporated in the in-tree virtualenv.
 xpidl_dir = mozpath.join(buildconfig.topsrcdir, 'xpcom', 'idl-parser',
--- a/browser/actors/ContextMenuChild.jsm
+++ b/browser/actors/ContextMenuChild.jsm
@@ -120,16 +120,17 @@ class ContextMenuChild extends JSWindowA
                 media.setAttribute("controls", "true");
                 break;
               case "fullscreen":
                 if (this.document.fullscreenEnabled) {
                   media.requestFullscreen();
                 }
                 break;
               case "pictureinpicture":
+                Services.telemetry.keyedScalarAdd("pictureinpicture.opened_method", "contextmenu", 1);
                 let event = new this.contentWindow.CustomEvent("MozTogglePictureInPicture", {
                   bubbles: true,
                 }, this.contentWindow);
                 media.dispatchEvent(event);
                 break;
             }
           }
         );
--- a/browser/app/blocklist.xml
+++ b/browser/app/blocklist.xml
@@ -1,10 +1,10 @@
 <?xml version='1.0' encoding='UTF-8'?>
-<blocklist lastupdate="1561554563187" xmlns="http://www.mozilla.org/2006/addons-blocklist">
+<blocklist lastupdate="1561710520557" xmlns="http://www.mozilla.org/2006/addons-blocklist">
   <emItems>
     <emItem blockID="i334" id="{0F827075-B026-42F3-885D-98981EE7B1AE}">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
     <emItem blockID="i1211" id="flvto@hotger.com">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="1"/>
@@ -3248,16 +3248,32 @@
     <emItem blockID="2e886e0c-00ab-44c2-bcbe-7c8793d46d89" id="/^((\{4200b565-5c4a-410f-b4fb-478daae2537f\})|(\{a0bba103-21d5-49c8-96f3-4eabbe78ced3\})|(\{ec46fe21-5690-4757-8ebc-1c77f826fe6b\})|(\{ce45d605-3bb6-4fad-8c1b-238ecee0d3df\})|(\{c70bd1fe-1d7d-4ae5-a731-3d513e6c46ba\})|(\{aeec96ca-81b9-405c-bd70-01ea6a50be9d\})|(\{0a1603a8-839f-4890-b1e3-1b8e00a7a0c9\})|(\{45febc8f-eaeb-4cec-90ea-07a7edc22793\})|(\{a7c7febd-6726-4d0e-9947-2edfd8bea35a\})|(\{eda3389e-ae07-4a2c-9b50-ce4e9008f297\})|(\{0e5d1d65-4fbb-4dd9-9042-3b568d9e2489\})|(\{1461f0e5-3c4a-453e-aed2-ca45ff5db747\})|(\{e842e73d-9d8a-45a8-bf0d-ef974ab24767\})|(\{e1d4fa8a-3da0-4fee-8b4f-0c7233fcb49a\}))$/">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
     <emItem blockID="0c227983-1180-4b4a-b25b-8160738e7238" id="/^((\{f0df6aa3-9792-4476-daa6-4709f93bbce3\})|(\{fe934134-3d0f-462b-d56e-e7187dfa8c98\})|(\{429999c4-1b8b-46fb-863f-ce19a08afc9c\})|(\{b8003074-2123-45be-91cf-654ef9671e1a\})|(\{9712066a-d491-4293-cd31-8ef8ee907d40\})|(\{dcfbb98b-783b-4df0-8427-e269114736cb\})|(\{66c44e3b-2df2-4741-ff07-0067cca4fe95\})|(\{af0a4d96-3403-496f-9d9a-5c766bf44bac\})|(\{82c60958-45da-4e6a-de21-879775c5473a\})|(\{c9118234-5787-488d-b30c-7d0a904fbabb\})|(\{f07d3da6-81ea-464f-9bef-6ff5470b307b\})|(\{c2454a12-7f57-440e-f695-0a9618f48b80\})|(\{f6e1d884-8100-49e7-88b9-bff8d9295cd2\}))$/">
       <prefs/>
       <versionRange minVersion="0" maxVersion="*" severity="3"/>
     </emItem>
+    <emItem blockID="3e88dad8-f640-46dd-8b00-4b955eea7b24" id="/^((\{65b88db7-9c07-4d03-80eb-2e5cf6cd7aa8\})|(\{aa2ef90f-db17-4ece-abab-4f87830457db\})|(\{e50969c9-088c-4978-9ffb-5d78015dabcc\})|(\{15fd1a8e-db53-41fa-9c46-93ec5b7473c1\})|(\{ed84b63e-faa2-4c48-b080-e9612cbc2e49\})|(\{c784f63e-5609-47a8-92ee-33a2bcb3239b\})|(\{1641b1ec-9a3d-4e3c-b52e-bc80671349f9\}))$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
+    <emItem blockID="bea9680c-28c0-48a1-b8d4-e418adeba748" id="/^((@searchincognito)|(@si-defaultsearch)|(@si-defaultsearch-listed)|(@searchassistincognito)|(@searchencrypt)|(@DiscreteSearch)|(@Discrete-Search)|(@searchsafe)|(@SearchSafeOrg)|(ffredirector@discretesearch\.com)|(ffredirector@encryptedsearch\.org)|(ffredirector@searchdefence\.com)|(ffredirector@searchencrypt\.com)|(ffredirector@searchencrypted\.com)|(ffredirector@searchincognito\.com)|(ffredirector@searchsafe\.co)|(ff_redirector@discretesearch\.com)|(ff_redirector@encryptedsearch\.org)|(ff_redirector@searchdefence\.com)|(ff_redirector@searchencrypt\.com)|(ff_redirector@searchencrypted\.com)|(ff_redirector@searchincognito\.com)|(ff_redirector@searchsafe\.co)|(@encryptedsearch)|(@searchdefence)|(@searchencrypted)|(@42e62954-834c-11e7-bb31-be2e44b06b34)|(@DiscreteSearchx)|(@4aec09f1-f1c9-456d-8c40-e0e86f302a0d)|(@566ff1c3-9432-4ed4-bd3d-b43cba47e051)|(@1df4e663-b9f3-4708-9f5d-44265b33397e)|(ff_redirector@searchsafe)|(\{9b62bdf8-a3c7-43d3-ba7f-0970cabffdaa\})|(\{95b48d11-b256-48ad-8ba1-bfe52f0a8bb8\})|(\{9e35a2be-64bd-49e3-aa47-fbeedf1834eb\})|(\{3ba10b5f-d9fa-4b40-8683-034d3dfc71d4\})|(\{20c31601-ebee-4677-a2f0-40e178bf7c77\})|(\{98e02622-f905-434e-9003-6c061b5c11c0\})|(@tabwow)|(gaidpiakchgkapdgbnoglpnbccdepnpk@chrome-store-foxified-258456913)|(@tabwow2)|(\{be8901e4-2a07-4554-aa05-a64351496e29\})|(moviestmpd@mozilla\.com)|(gaidpiakchgkapdgbnoglpnbccdepnpk@chrome-store-foxified-876542484)|(\{4a8ef415-e453-458f-bfbd-ae92569214db\})|(fireaction@mozilla\.com)|(\{bd9c448c-58b3-434f-9bb6-4ed2c155ba8e\})|(\{ebdfa19b-0906-4f78-9e95-7ef74d34c335\})|(websecure-unlisted@mozilla\.com)|(\{2d06d70b-8f32-4007-8f8b-1e0445bcebe7\})|(\{ddbe7345-acf4-4ebb-9baf-cd6d2df73b28\})|(\{b09d5b98-2d65-46fb-990c-69710577efa0\})|(\{3894384e-c719-4a0c-8d24-3816160fc56b\})|(search-encrypt-tab@mozilla\.xpi)|(\{1dafa1da-3894-48b9-ac8f-00bdc4f1868a\})|(\{99cfe634-328a-41a5-9a23-64094e4f4919\})|(inco-plugin@mozilla\.xpi)|(incognito-window@mozilla\.xpi)|(mac-search@mozilla\.xpi)|(fvdplayer@fvd\.com)|(playernewpp@ext\.com)|(\{492936c6-9121-4e54-8d4f-97f544e5bf98\})|(\{108a22ea-f316-4c2f-8427-fe65e02f9e2c\})|(cold@being\.net)|(\{38b99237-6c28-406f-898c-cc89df86051d\})|(search_redirect@mozilla\.xpi)|(\{d2ef4a8d-6ec0-4733-9f3f-2394178ecbf3\})|(tab_plugin@mozilla\.xpi)|(\{ae228e30-f40a-41a3-9e7e-53a094dcb8c6\})|(\{00ee7237-53cb-4036-8d4f-e78d78ca89e7\})|(\{d2f4002c-031b-4ad3-9fb1-afb003e8f932\})|(\{c0f366b3-7b3d-4486-a6f3-4ca1d7045091\})|(\{ccc6cfc4-3832-4d05-bf28-43a9722de93f\})|(\{dd02f638-ce6d-464e-8add-6ea0f314b1d1\})|(\{749ed3ff-4d23-4b32-812e-a35e3cf8c000\})|(tab_cleanup@mozilla\.xpi)|(incognito_tab@mozilla\.xpi)|(\{47c51f55-4f0b-499f-9fdd-c7c66bf4796a\})|(\{cd70c7c8-557d-46fa-9688-399c7c8d3d66\})|(\{681ad8e0-d1df-4cd2-a4cf-b97c1d6502a3\})|(\{0d58e690-bd48-4e3a-baf3-67aa40bc286a\})|(\{77bfbf26-4618-4120-9cb6-1fc7c92b8ddc\})|(\{037c6f6a-71f8-405b-9cff-fadf2ded6c47\})|(\{91cc3274-90d5-4e16-80e3-cd02fc513689\})|(\{2225b2af-0c3c-4345-adac-4f5bd40c2182\})|(\{81ca6b1e-a95b-4b44-9638-3ff3ea1a571d\})|(\{1e32acf8-fc1e-40ae-8783-c501ce50d597\})|(\{19670785-b1db-4d69-9538-2880ad8fdf20\})|(\{0113b4ad-15ca-4215-adeb-f0404f619ca6\})|(\{c7245149-4224-4c5c-91a4-84ea189f2227\})|(\{04dd2232-f1b1-4275-ae74-8bd27f3d850c\})|(prosearch@mozilla\.xpi)|(\{d549a064-98e7-49ed-ba9e-a724e79a004f\})|(\{fddd3bc6-9d4e-4ee7-b490-0d6141ff7d7f\})|(\{122795b5-ae28-4371-9b61-878f5db888ac\})|(\{e3d491de-802a-4f82-91eb-9403c9f43637\}))$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
+    <emItem blockID="3460b6b7-8251-4588-8f12-81ac8d288c88" id="/^((\{b7a0ecf9-212b-49ca-bec1-ead0c6dc2128\})|(\{6e977a6d-b31d-4735-a586-41dc25df2100\})|(\{67155a2a-6538-42b1-bdc9-f48b442f57e7\})|(\{b4d4abc0-5e6e-4a34-a7e3-bfe7319160b8\})|(\{2102c5a9-f3c4-4f71-bb6e-c45c9d07b6c8\})|(\{071c1c7a-cde3-4a22-aefe-7c8f98df6277\})|(\{aa2f3e70-7dcf-4b4e-92c5-54c8280b98de\})|(\{3b376420-b276-4a0c-9e28-340dcc0547ce\})|(\{ed192371-abcc-4520-ab76-d22afbe51dff\})|(\{ad5a457f-59c8-4d90-8e3e-59f13a3bc2b2\})|(\{06aa60ab-91ad-4b8a-bfda-98e33b65fbb5\})|(\{c2875a12-da6a-4f90-a919-1d2bef57fbff\})|(\{b01d1c5b-58b5-4411-86d0-555131c7bd07\})|(\{0a79c7eb-5fe9-4e37-841e-18686bc86a20\})|(\{341ca205-d6e0-4d03-93be-04939c429412\})|(\{855e09d9-ac3a-4885-828d-557734060c1f\})|(\{8ac01eb1-9819-4c41-b2b7-042d8cdb3f2e\}))$/">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
+    <emItem blockID="9f484302-44da-4b6a-afd8-94113b83c0f6" id="tab-api@mozilla.xpi">
+      <prefs/>
+      <versionRange minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
   </emItems>
   <pluginItems>
     <pluginItem blockID="p332">
       <match exp="libflashplayer\.so" name="filename"/>
       <match exp="^Shockwave Flash 11.(0|1) r[0-9]{1,3}$" name="description"/>
       <infoURL>https://get.adobe.com/flashplayer/</infoURL>
       <versionRange severity="0" vulnerabilitystatus="1">
         <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
--- a/browser/app/macversion.py
+++ b/browser/app/macversion.py
@@ -1,14 +1,14 @@
 #!/usr/bin/python
 # 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/.
 
-
+from __future__ import absolute_import, print_function
 from optparse import OptionParser
 import sys
 import re
 
 o = OptionParser()
 o.add_option("--buildid", dest="buildid")
 o.add_option("--version", dest="version")
 
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -487,16 +487,19 @@ pref("browser.tabs.delayHidingAudioPlayi
 // for about: pages. This pref name did not age well: we will have multiple
 // types of privileged content processes, each with different privileges.
 // types of privleged content processes, each with different privleges.
 pref("browser.tabs.remote.separatePrivilegedContentProcess", true);
 // Pref to control whether we use a separate privileged content process
 // for certain mozilla webpages (which are listed in the pref
 // browser.tabs.remote.separatedMozillaDomains).
 pref("browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", false);
+// This pref will cause assertions when a remoteType triggers a process switch
+// to a new remoteType it should not be able to trigger.
+pref("browser.tabs.remote.enforceRemoteTypeRestrictions", true);
 #endif
 
 #ifdef NIGHTLY_BUILD
 pref("browser.tabs.remote.useHTTPResponseProcessSelection", true);
 #else
 // Disabled outside of nightly due to bug 1554217
 pref("browser.tabs.remote.useHTTPResponseProcessSelection", false);
 #endif
@@ -1312,16 +1315,19 @@ pref("browser.newtabpage.activity-stream
 #ifdef NIGHTLY_BUILD
 pref("browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", true);
 #else
 pref("browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", false);
 #endif
 
 pref("trailhead.firstrun.branches", "join-privacy");
 
+// The pref that controls if the What's New panel is enabled.
+pref("browser.messaging-system.whatsNewPanel.enabled", false);
+
 // Enable the DOM fullscreen API.
 pref("full-screen-api.enabled", true);
 
 // Startup Crash Tracking
 // number of startup crashes that can occur before starting into safe mode automatically
 // (this pref has no effect if more than 6 hours have passed since the last crash)
 pref("toolkit.startup.max_resumed_crashes", 3);
 
@@ -1733,17 +1739,18 @@ pref("extensions.pocket.oAuthConsumerKey
 pref("extensions.pocket.site", "getpocket.com");
 
 pref("signon.schemeUpgrades", true);
 pref("signon.privateBrowsingCapture.enabled", true);
 pref("signon.showAutoCompleteFooter", true);
 pref("signon.management.page.enabled", false);
 pref("signon.showAutoCompleteOrigins", true);
 pref("signon.includeOtherSubdomainsInLookup", true);
-pref("signon.feedbackURL",
+pref("signon.management.page.faqURL", "https://lockwise.firefox.com/faq.html");
+pref("signon.management.page.feedbackURL",
      "https://www.surveygizmo.com/s3/5036102/Lockwise-feedback?ver=%VERSION%");
 
 // Enable the "Simplify Page" feature in Print Preview. This feature
 // is disabled by default in toolkit.
 pref("print.use_simplify_page", true);
 
 // Space separated list of URLS that are allowed to send objects (instead of
 // only strings) through webchannels. This list is duplicated in mobile/android/app/mobile.js
--- a/browser/base/content/aboutRestartRequired.js
+++ b/browser/base/content/aboutRestartRequired.js
@@ -26,8 +26,13 @@ var AboutRestartRequired = {
                           Ci.nsIAppStartup.eAttemptQuit);
   },
   init() {
     this.addAutofocus();
   },
 };
 
 AboutRestartRequired.init();
+
+let restartButton = document.getElementById("restart");
+restartButton.onclick = function() {
+  AboutRestartRequired.restart();
+};
--- a/browser/base/content/aboutRestartRequired.xhtml
+++ b/browser/base/content/aboutRestartRequired.xhtml
@@ -1,16 +1,17 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 <!-- 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/. -->
 <!DOCTYPE html>
 
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
+    <meta http-equiv="Content-Security-Policy" content="default-src chrome:" />
     <title data-l10n-id="restart-required-title"></title>
     <link rel="stylesheet" type="text/css" media="all"
           href="chrome://browser/skin/aboutRestartRequired.css"/>
     <!-- If the location of the favicon is changed here, the
          FAVICON_ERRORPAGE_URL symbol in
          toolkit/components/places/src/nsFaviconService.h should be updated. -->
     <link rel="icon" type="image/png" id="favicon"
           href="chrome://global/skin/icons/warning.svg"/>
@@ -28,15 +29,14 @@
           <div id="errorLongDesc">
             <p data-l10n-id="restart-required-intro-brand"></p>
             <p data-l10n-id="restart-required-description"></p>
           </div>
         </div>
       </div>
       <!-- Restart Button -->
       <div id="restartButtonContainer" class="button-container">
-        <button id="restart" data-l10n-id="restart-button-label" class="primary" autocomplete="off"
-                onclick="AboutRestartRequired.restart();"></button>
+        <button id="restart" data-l10n-id="restart-button-label" class="primary" autocomplete="off"></button>
       </div>
     </div>
   </body>
   <script src="chrome://browser/content/aboutRestartRequired.js"/>
 </html>
--- a/browser/base/content/browser-contentblocking.js
+++ b/browser/base/content/browser-contentblocking.js
@@ -966,46 +966,52 @@ var ContentBlocking = {
   openPreferences(origin) {
     openPreferences("privacy-trackingprotection", { origin });
   },
 
   backToMainView() {
     this.identityPopupMultiView.goBack();
   },
 
-  submitBreakageReport() {
+  onSubmitBreakageReportClicked() {
     this.identityPopup.hidePopup();
 
+    let comments = document.getElementById(
+      "identity-popup-breakageReportView-collection-comments");
+    this.submitBreakageReport(this.reportURI, comments);
+  },
+
+  submitBreakageReport(uri, commentsTextarea) {
     let reportEndpoint = Services.prefs.getStringPref(this.PREF_REPORT_BREAKAGE_URL);
     if (!reportEndpoint) {
       return;
     }
 
     let formData = new FormData();
-    formData.set("title", this.reportURI.host);
+    formData.set("title", uri.host);
 
     // Leave the ? at the end of the URL to signify that this URL had its query stripped.
-    let urlWithoutQuery = this.reportURI.asciiSpec.replace(this.reportURI.query, "");
+    let urlWithoutQuery = uri.asciiSpec.replace(uri.query, "");
     let body = `Full URL: ${urlWithoutQuery}\n`;
     body += `userAgent: ${navigator.userAgent}\n`;
 
     body += "\n**Preferences**\n";
     body += `${TrackingProtection.PREF_ENABLED_GLOBALLY}: ${Services.prefs.getBoolPref(TrackingProtection.PREF_ENABLED_GLOBALLY)}\n`;
     body += `${TrackingProtection.PREF_ENABLED_IN_PRIVATE_WINDOWS}: ${Services.prefs.getBoolPref(TrackingProtection.PREF_ENABLED_IN_PRIVATE_WINDOWS)}\n`;
     body += `urlclassifier.trackingTable: ${Services.prefs.getStringPref("urlclassifier.trackingTable")}\n`;
     body += `network.http.referer.defaultPolicy: ${Services.prefs.getIntPref("network.http.referer.defaultPolicy")}\n`;
     body += `network.http.referer.defaultPolicy.pbmode: ${Services.prefs.getIntPref("network.http.referer.defaultPolicy.pbmode")}\n`;
     body += `${ThirdPartyCookies.PREF_ENABLED}: ${Services.prefs.getIntPref(ThirdPartyCookies.PREF_ENABLED)}\n`;
     body += `network.cookie.lifetimePolicy: ${Services.prefs.getIntPref("network.cookie.lifetimePolicy")}\n`;
+    body += `privacy.annotate_channels.strict_list.enabled: ${Services.prefs.getBoolPref("privacy.annotate_channels.strict_list.enabled")}\n`;
     body += `privacy.restrict3rdpartystorage.expiration: ${Services.prefs.getIntPref("privacy.restrict3rdpartystorage.expiration")}\n`;
     body += `${Fingerprinting.PREF_ENABLED}: ${Services.prefs.getBoolPref(Fingerprinting.PREF_ENABLED)}\n`;
     body += `${Cryptomining.PREF_ENABLED}: ${Services.prefs.getBoolPref(Cryptomining.PREF_ENABLED)}\n`;
 
-    let comments = document.getElementById("identity-popup-breakageReportView-collection-comments");
-    body += "\n**Comments**\n" + comments.value;
+    body += "\n**Comments**\n" + commentsTextarea.value;
 
     formData.set("body", body);
 
     let activatedBlockers = [];
     for (let blocker of this.blockers) {
       if (blocker.activated) {
         activatedBlockers.push(blocker.reportBreakageLabel);
       }
@@ -1019,17 +1025,17 @@ var ContentBlocking = {
       method: "POST",
       credentials: "omit",
       body: formData,
     }).then(function(response) {
       if (!response.ok) {
         Cu.reportError(`Content Blocking report to ${reportEndpoint} failed with status ${response.status}`);
       } else {
         // Clear the textarea value when the report is submitted
-        comments.value = "";
+        commentsTextarea.value = "";
       }
     }).catch(Cu.reportError);
   },
 
   toggleReportBreakageButton() {
     // For release (due to the large volume) we only want to receive reports
     // for breakage that is directly related to third party cookie blocking.
     if (this.reportBreakageEnabled ||
--- a/browser/base/content/browser-siteProtections.js
+++ b/browser/base/content/browser-siteProtections.js
@@ -12,21 +12,36 @@ var gProtectionsHandler = {
   get _protectionsPopup() {
     delete this._protectionsPopup;
     return this._protectionsPopup = document.getElementById("protections-popup");
   },
   get _protectionsIconBox() {
     delete this._protectionsIconBox;
     return this._protectionsIconBox = document.getElementById("tracking-protection-icon-animatable-box");
   },
+  get _protectionsPopupMultiView() {
+    delete this._protectionsPopupMultiView;
+    return this._protectionsPopupMultiView =
+      document.getElementById("protections-popup-multiView");
+  },
+  get _protectionsPopupMainView() {
+    delete this._protectionsPopupMainView;
+    return this._protectionsPopupMainView =
+      document.getElementById("protections-popup-mainView");
+  },
   get _protectionsPopupMainViewHeaderLabel() {
     delete this._protectionsPopupMainViewHeaderLabel;
     return this._protectionsPopupMainViewHeaderLabel =
       document.getElementById("protections-popup-mainView-panel-header-span");
   },
+  get _protectionsPopupTPSwitchBreakageLink() {
+    delete this._protectionsPopupTPSwitchBreakageLink;
+    return this._protectionsPopupTPSwitchBreakageLink =
+      document.getElementById("protections-popup-tp-switch-breakage-link");
+  },
   get _protectionsPopupTPSwitch() {
     delete this._protectionsPopupTPSwitch;
     return this._protectionsPopupTPSwitch =
       document.getElementById("protections-popup-tp-switch");
   },
   get _protectionPopupSettingsButton() {
     delete this._protectionPopupSettingsButton;
     return this._protectionPopupSettingsButton =
@@ -37,82 +52,126 @@ var gProtectionsHandler = {
     return this._protectionPopupFooter =
       document.getElementById("protections-popup-footer");
   },
   get _protectionPopupTrackersCounterDescription() {
     delete this._protectionPopupTrackersCounterDescription;
     return this._protectionPopupTrackersCounterDescription =
       document.getElementById("protections-popup-trackers-blocked-counter-description");
   },
+  get _protectionsPopupSiteNotWorkingTPSwitch() {
+    delete this._protectionsPopupSiteNotWorkingTPSwitch;
+    return this._protectionsPopupSiteNotWorkingTPSwitch =
+      document.getElementById("protections-popup-siteNotWorking-tp-switch");
+  },
+  get _protectionsPopupSendReportLearnMore() {
+    delete this._protectionsPopupSendReportLearnMore;
+    return this._protectionsPopupSendReportLearnMore =
+      document.getElementById("protections-popup-sendReportView-learn-more");
+  },
+  get _protectionsPopupSendReportURL() {
+    delete this._protectionsPopupSendReportURL;
+    return this._protectionsPopupSendReportURL =
+      document.getElementById("protections-popup-sendReportView-collection-url");
+  },
+  get _protectionsPopupToastTimeout() {
+    delete this._protectionsPopupToastTimeout;
+    XPCOMUtils.defineLazyPreferenceGetter(this, "_protectionsPopupToastTimeout",
+                                          "browser.protections_panel.toast.timeout",
+                                          5000);
+    return this._protectionsPopupToastTimeout;
+  },
 
   handleProtectionsButtonEvent(event) {
     event.stopPropagation();
     if ((event.type == "click" && event.button != 0) ||
         (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE &&
          event.keyCode != KeyEvent.DOM_VK_RETURN)) {
       return; // Left click, space or enter only
     }
 
-    // Make sure that the display:none style we set in xul is removed now that
-    // the popup is actually needed
-    this._protectionsPopup.hidden = false;
-
-    // Refresh strings.
-    this.refreshProtectionsPopup();
-
-    // Now open the popup, anchored off the primary chrome element
-    PanelMultiView.openPopup(this._protectionsPopup, gIdentityHandler._identityIcon, {
-      position: "bottomcenter topleft",
-      triggerEvent: event,
-    }).catch(Cu.reportError);
+    this.showProtectionsPopup({event});
   },
 
   onPopupShown(event) {
     if (event.target == this._protectionsPopup) {
       window.addEventListener("focus", this, true);
     }
   },
 
   onPopupHidden(event) {
     if (event.target == this._protectionsPopup) {
       window.removeEventListener("focus", this, true);
       this._protectionsPopup.removeAttribute("open");
     }
   },
 
+  onHeaderClicked(event) {
+    // Display the whole protections panel if the toast has been clicked.
+    if (this._protectionsPopup.hasAttribute("toast")) {
+      // Hide the toast first.
+      PanelMultiView.hidePopup(this._protectionsPopup);
+
+      // Open the full protections panel.
+      this.showProtectionsPopup({event});
+    }
+  },
+
+  onLocationChange() {
+    if (!this._showToastAfterRefresh) {
+      return;
+    }
+
+    this._showToastAfterRefresh = false;
+
+    // We only display the toast if we're still on the same page.
+    if (this._previousURI != gBrowser.currentURI.spec ||
+        this._previousOuterWindowID != gBrowser.selectedBrowser.outerWindowID) {
+      return;
+    }
+
+    this.showProtectionsPopup({
+      toast: true,
+    });
+  },
+
   handleEvent(event) {
     let elem = document.activeElement;
     let position = elem.compareDocumentPosition(this._protectionsPopup);
 
     if (!(position & (Node.DOCUMENT_POSITION_CONTAINS |
                       Node.DOCUMENT_POSITION_CONTAINED_BY)) &&
         !this._protectionsPopup.hasAttribute("noautohide")) {
       // Hide the panel when focusing an element that is
       // neither an ancestor nor descendant unless the panel has
       // @noautohide (e.g. for a tour).
       PanelMultiView.hidePopup(this._protectionsPopup);
     }
   },
 
   refreshProtectionsPopup() {
-    // Refresh the state of the TP toggle switch.
-    this._protectionsPopupTPSwitch.toggleAttribute("enabled",
-      !this._protectionsPopup.hasAttribute("hasException"));
-
     let host = gIdentityHandler.getHostForDisplay();
 
     // Push the appropriate strings out to the UI.
     this._protectionsPopupMainViewHeaderLabel.textContent =
       // gNavigatorBundle.getFormattedString("protections.header", [host]);
       `Tracking Protections for ${host}`;
 
     let currentlyEnabled =
       !this._protectionsPopup.hasAttribute("hasException");
 
-    this._protectionsPopupTPSwitch.toggleAttribute("enabled", currentlyEnabled);
+    for (let tpSwitch of [this._protectionsPopupTPSwitch,
+                          this._protectionsPopupSiteNotWorkingTPSwitch]) {
+      tpSwitch.toggleAttribute("enabled", currentlyEnabled);
+    }
+
+    // Display the breakage link according to the current enable state.
+    // The display state of the breakage link will be fixed once the protections
+    // panel opened no matter how the TP switch state is.
+    this._protectionsPopupTPSwitchBreakageLink.hidden = !currentlyEnabled;
 
     // Set the counter of the 'Trackers blocked This Week'.
     // We need to get the statistics of trackers. So far, we haven't implemented
     // this yet. So we use a fake number here. Should be resolved in
     // Bug 1555231.
     this.setTrackersBlockedCounter(244051);
   },
 
@@ -122,23 +181,35 @@ var gProtectionsHandler = {
     // We need to ensure we don't handle more clicks during the 500ms delay,
     // so we keep track of state and return early if needed.
     if (this._TPSwitchCommanding) {
       return;
     }
 
     this._TPSwitchCommanding = true;
 
-    let currentlyEnabled =
-      !this._protectionsPopup.hasAttribute("hasException");
+    // Toggling the 'hasException' on the protections panel in order to do some
+    // styling after toggling the TP switch.
+    let newExceptionState =
+      this._protectionsPopup.toggleAttribute("hasException");
+    for (let tpSwitch of [this._protectionsPopupTPSwitch,
+                          this._protectionsPopupSiteNotWorkingTPSwitch]) {
+      tpSwitch.toggleAttribute("enabled", !newExceptionState);
+    }
 
-    this._protectionsPopupTPSwitch.toggleAttribute("enabled", !currentlyEnabled);
+    // Indicating that we need to show a toast after refreshing the page.
+    // And caching the current URI and window ID in order to only show the mini
+    // panel if it's still on the same page.
+    this._showToastAfterRefresh = true;
+    this._previousURI = gBrowser.currentURI.spec;
+    this._previousOuterWindowID = gBrowser.selectedBrowser.outerWindowID;
+
     await new Promise((resolve) => setTimeout(resolve, 500));
 
-    if (currentlyEnabled) {
+    if (newExceptionState) {
       ContentBlocking.disableForCurrentPage();
       gIdentityHandler.recordClick("unblock");
     } else {
       ContentBlocking.enableForCurrentPage();
       gIdentityHandler.recordClick("block");
     }
 
     PanelMultiView.hidePopup(this._protectionsPopup);
@@ -146,9 +217,80 @@ var gProtectionsHandler = {
   },
 
   setTrackersBlockedCounter(trackerCount) {
     this._protectionPopupTrackersCounterDescription.textContent =
       // gNavigatorBundle.getFormattedString(
       //   "protections.trackers_counter", [cnt]);
       `Trackers blocked this week: ${trackerCount.toLocaleString()}`;
   },
+
+  /**
+   * Showing the protections popup.
+   *
+   * @param {Object} options
+   *                 The object could have two properties.
+   *                 event:
+   *                   The event triggers the protections popup to be opened.
+   *                 toast:
+   *                   A boolean to indicate if we need to open the protections
+   *                   popup as a toast. A toast only has a header section and
+   *                   will be hidden after a certain amount of time.
+   */
+  showProtectionsPopup(options = {}) {
+    const {event, toast} = options;
+
+    // We need to clear the toast timer if it exists before showing the
+    // protections popup.
+    if (this._toastPanelTimer) {
+      clearTimeout(this._toastPanelTimer);
+      delete this._toastPanelTimer;
+    }
+
+    // Make sure that the display:none style we set in xul is removed now that
+    // the popup is actually needed
+    this._protectionsPopup.hidden = false;
+
+    this._protectionsPopup.toggleAttribute("toast", !!toast);
+    if (!toast) {
+      // Refresh strings if we want to open it as a standard protections popup.
+      this.refreshProtectionsPopup();
+    }
+
+    if (toast) {
+      this._protectionsPopup.addEventListener("popupshown", () => {
+        this._toastPanelTimer = setTimeout(() => {
+          PanelMultiView.hidePopup(this._protectionsPopup);
+          delete this._toastPanelTimer;
+        }, this._protectionsPopupToastTimeout);
+      }, {once: true});
+    }
+
+    // Now open the popup, anchored off the primary chrome element
+    PanelMultiView.openPopup(this._protectionsPopup, gIdentityHandler._identityIcon, {
+      position: "bottomcenter topleft",
+      triggerEvent: event,
+    }).catch(Cu.reportError);
+  },
+
+  showSiteNotWorkingView() {
+    this._protectionsPopupMultiView.showSubView("protections-popup-siteNotWorkingView");
+  },
+
+  showSendReportView() {
+    // Save this URI to make sure that the user really only submits the location
+    // they see in the report breakage dialog.
+    this.reportURI = gBrowser.currentURI;
+    let urlWithoutQuery = this.reportURI.asciiSpec.replace("?" + this.reportURI.query, "");
+    this._protectionsPopupSendReportURL.value = urlWithoutQuery;
+    this._protectionsPopupMultiView.showSubView("protections-popup-sendReportView");
+  },
+
+  onSendReportClicked() {
+    this._protectionsPopup.hidePopup();
+    let comments = document.getElementById(
+      "protections-popup-sendReportView-collection-comments");
+    ContentBlocking.submitBreakageReport(this.reportURI, comments);
+  },
 };
+
+let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+gProtectionsHandler._protectionsPopupSendReportLearnMore.href = baseURL + "blocking-breakage";
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -223,17 +223,19 @@ var gURLBarHandler = {
 
   /**
    * Forwards to gURLBar.formatValue(), if the binding has been applied already.
    * This is necessary until the Quantum Bar is not the default and we allow
    * to dynamically switch between it and the legacy implementation, because the
    * binding is only applied before the initial xul layout.
    */
   formatValue() {
-    if (typeof this.textbox.formatValue == "function") {
+    if (this.quantumbar) {
+      this.urlbar.formatValue();
+    } else if (typeof this.textbox.formatValue == "function") {
       this.textbox.formatValue();
     }
   },
 
   /**
    * Invoked when the quantumbar pref changes.
    */
   handlePrefChange() {
@@ -4893,16 +4895,18 @@ var XULBrowserWindow = {
       // if this is a document navigation then PopupNotifications will be updated
       // via TabsProgressListener.onLocationChange and we do not want it called twice
       URLBarSetURI(aLocationURI, aIsSimulated);
 
       BookmarkingUI.onLocationChange();
 
       gIdentityHandler.onLocationChange();
 
+      gProtectionsHandler.onLocationChange();
+
       BrowserPageActions.onLocationChange();
 
       SafeBrowsingNotificationBox.onLocationChange(aLocationURI);
 
       gTabletModePageCounter.inc();
 
       this._updateElementsForContentType();
 
--- a/browser/base/content/test/fullscreen/browser.ini
+++ b/browser/base/content/test/fullscreen/browser.ini
@@ -1,4 +1,5 @@
 [DEFAULT]
 support-files =
   head.js
 [browser_bug1557041.js]
+skip-if = os == 'linux' # Bug 1561973
--- a/browser/base/content/test/siteProtections/browser_protections_UI.js
+++ b/browser/base/content/test/siteProtections/browser_protections_UI.js
@@ -2,35 +2,65 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* Basic UI tests for the protections panel */
 
 add_task(async function setup() {
   await SpecialPowers.pushPrefEnv({
     set: [
       ["browser.protections_panel.enabled", true],
+      // Set the auto hide timing to 100ms for blocking the test less.
+      ["browser.protections_panel.toast.timeout", 100],
     ],
   });
 });
 
 add_task(async function testToggleSwitch() {
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com");
   await openProtectionsPanel();
   ok(gProtectionsHandler._protectionsPopupTPSwitch.hasAttribute("enabled"), "TP Switch should be enabled");
   let popuphiddenPromise = BrowserTestUtils.waitForEvent(protectionsPopup, "popuphidden");
   let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
   gProtectionsHandler._protectionsPopupTPSwitch.click();
   await popuphiddenPromise;
+
+  // We need to wait toast's popup shown and popup hidden events. It won't fire
+  // the popup shown event if we open the protections panel while the toast is
+  // opening.
+  let popupShownPromise = BrowserTestUtils.waitForEvent(protectionsPopup, "popupshown");
+  popuphiddenPromise = BrowserTestUtils.waitForEvent(protectionsPopup, "popuphidden");
+
   await browserLoadedPromise;
+
+  // Wait until the toast is shown and hidden.
+  await popupShownPromise;
+  await popuphiddenPromise;
+
   await openProtectionsPanel();
   ok(!gProtectionsHandler._protectionsPopupTPSwitch.hasAttribute("enabled"), "TP Switch should be disabled");
   Services.perms.remove(ContentBlocking._baseURIForChannelClassifier, "trackingprotection");
   BrowserTestUtils.removeTab(tab);
 });
 
+add_task(async function testSiteNotWorking() {
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com");
+  await openProtectionsPanel();
+  let viewShownPromise = BrowserTestUtils.waitForEvent(
+    gProtectionsHandler._protectionsPopupMultiView, "ViewShown");
+  document.getElementById("protections-popup-tp-switch-breakage-link").click();
+  let event = await viewShownPromise;
+  is(event.originalTarget.id, "protections-popup-siteNotWorkingView", "Site Not Working? view should be shown");
+  viewShownPromise = BrowserTestUtils.waitForEvent(
+    gProtectionsHandler._protectionsPopupMultiView, "ViewShown");
+  document.getElementById("protections-popup-siteNotWorkingView-sendReport").click();
+  event = await viewShownPromise;
+  is(event.originalTarget.id, "protections-popup-sendReportView", "Send Report view should be shown");
+  BrowserTestUtils.removeTab(tab);
+});
+
 /**
  * A test for the protection settings button.
  */
 add_task(async function testSettingsButton() {
   // Open a tab and its protection panel.
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com");
   await openProtectionsPanel();
 
@@ -47,17 +77,17 @@ add_task(async function testSettingsButt
 
   BrowserTestUtils.removeTab(newTab);
   BrowserTestUtils.removeTab(tab);
 });
 
 /**
  * A test for the 'Show Full Report' button in the footer seciton.
  */
-add_task(async function testShouFullReportLink() {
+add_task(async function testShowFullReportLink() {
   // Open a tab and its protection panel.
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com");
   await openProtectionsPanel();
 
   let popuphiddenPromise = BrowserTestUtils.waitForEvent(protectionsPopup, "popuphidden");
   let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:protections");
   let showFullReportLink =
     document.getElementById("protections-popup-show-full-report-link");
@@ -69,8 +99,94 @@ add_task(async function testShouFullRepo
   // Wait until the 'about:protections' has been opened correctly.
   let newTab = await newTabPromise;
 
   ok(true, "about:protections has been opened successfully");
 
   BrowserTestUtils.removeTab(newTab);
   BrowserTestUtils.removeTab(tab);
 });
+
+/**
+ * A test for ensuring the mini panel is working correctly
+ */
+add_task(async function testMiniPanel() {
+  // Open a tab.
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com");
+
+  // Open the mini panel.
+  await openProtectionsPanel(true);
+  let popuphiddenPromise = BrowserTestUtils.waitForEvent(protectionsPopup, "popuphidden");
+
+  // Check that only the header is displayed.
+  for (let item of protectionsPopupMainView.childNodes) {
+    if (item.id !== "protections-popup-mainView-panel-header") {
+      ok(!BrowserTestUtils.is_visible(item),
+         `The section '${item.id}' is hidden in the toast.`);
+    } else {
+      ok(BrowserTestUtils.is_visible(item),
+         "The panel header is displayed as the content of the toast.");
+    }
+  }
+
+  // Wait until the auto hide is happening.
+  await popuphiddenPromise;
+
+  ok(true, "The mini panel hides automatically.");
+
+  BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * A test for the toggle switch flow
+ */
+add_task(async function testToggleSwitchFlow() {
+  // Open a tab.
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com");
+  await openProtectionsPanel();
+
+  let popuphiddenPromise = BrowserTestUtils.waitForEvent(protectionsPopup, "popuphidden");
+  let popupShownPromise = BrowserTestUtils.waitForEvent(protectionsPopup, "popupshown");
+  let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+  // Click the TP switch, from On -> Off.
+  gProtectionsHandler._protectionsPopupTPSwitch.click();
+
+  // The panel should be closed and the mini panel will show up after refresh.
+  await popuphiddenPromise;
+  await browserLoadedPromise;
+  await popupShownPromise;
+
+  ok(gProtectionsHandler._protectionsPopup.hasAttribute("toast"),
+     "The protections popup should have the 'toast' attribute.");
+
+  // Click on the mini panel and making sure the protection popup shows up.
+  popupShownPromise = BrowserTestUtils.waitForEvent(protectionsPopup, "popupshown");
+  popuphiddenPromise = BrowserTestUtils.waitForEvent(protectionsPopup, "popuphidden");
+  protectionsPopupHeader.click();
+  await popuphiddenPromise;
+  await popupShownPromise;
+
+  ok(!gProtectionsHandler._protectionsPopup.hasAttribute("toast"),
+     "The 'toast' attribute should be cleared on the protections popup.");
+
+  // Click the TP switch again, from Off -> On.
+  popuphiddenPromise = BrowserTestUtils.waitForEvent(protectionsPopup, "popuphidden");
+  popupShownPromise = BrowserTestUtils.waitForEvent(protectionsPopup, "popupshown");
+  browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  gProtectionsHandler._protectionsPopupTPSwitch.click();
+
+  // Protections popup hidden -> Page refresh -> Mini panel shows up.
+  await popuphiddenPromise;
+  popuphiddenPromise = BrowserTestUtils.waitForEvent(protectionsPopup, "popuphidden");
+  await browserLoadedPromise;
+  await popupShownPromise;
+
+  ok(gProtectionsHandler._protectionsPopup.hasAttribute("toast"),
+     "The protections popup should have the 'toast' attribute.");
+
+  // Wait until the auto hide is happening.
+  await popuphiddenPromise;
+
+  // Clean up the TP state.
+  Services.perms.remove(ContentBlocking._baseURIForChannelClassifier, "trackingprotection");
+  BrowserTestUtils.removeTab(tab);
+});
--- a/browser/base/content/test/siteProtections/head.js
+++ b/browser/base/content/test/siteProtections/head.js
@@ -1,11 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var protectionsPopup = document.getElementById("protections-popup");
+var protectionsPopupMainView =
+  document.getElementById("protections-popup-mainView");
+var protectionsPopupHeader =
+  document.getElementById("protections-popup-mainView-panel-header");
 
-async function openProtectionsPanel() {
+async function openProtectionsPanel(toast) {
   let popupShownPromise = BrowserTestUtils.waitForEvent(protectionsPopup, "popupshown");
   let identityBox = document.getElementById("identity-box");
-  EventUtils.synthesizeMouseAtCenter(identityBox, { altKey: true });
+  if (!toast) {
+    EventUtils.synthesizeMouseAtCenter(identityBox, { altKey: true });
+  } else {
+    gProtectionsHandler.showProtectionsPopup({toast});
+  }
+
   await popupShownPromise;
 }
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -29,18 +29,16 @@ var gExceptionPaths = [
 
   // These resources are referenced using relative paths from html files.
   "resource://payments/",
   "resource://normandy-content/shield-content-frame.js",
   "resource://normandy-content/shield-content-process.js",
 
   // https://github.com/mozilla/activity-stream/issues/3053
   "resource://activity-stream/data/content/tippytop/images/",
-  // https://github.com/mozilla/activity-stream/issues/3758
-  "resource://activity-stream/prerendered/",
 
   // browser/extensions/pdfjs/content/build/pdf.js#1999
   "resource://pdf.js/web/images/",
 
   // Exclude all the metadata paths under the country metadata folder because these
   // paths will be concatenated in FormAutofillUtils.jsm based on different country/region.
   "resource://formautofill/addressmetadata/",
 
@@ -141,18 +139,16 @@ var whitelist = [
   {file: "chrome://mozapps/skin/downloads/buttons.png", platforms: ["macosx"]},
   {file: "chrome://mozapps/skin/downloads/downloadButtons.png", platforms: ["linux", "win"]},
   // Bug 1348559
   {file: "chrome://pippki/content/resetpassword.xul"},
   // Bug 1337345
   {file: "resource://gre/modules/Manifest.jsm"},
   // Bug 1351097
   {file: "resource://gre/modules/accessibility/AccessFu.jsm"},
-  // Bug 1356043
-  {file: "resource://gre/modules/PerfMeasurement.jsm"},
   // Bug 1356045
   {file: "chrome://global/content/test-ipc.xul"},
   // Bug 1378173 (warning: still used by devtools)
   {file: "resource://gre/modules/Promise.jsm"},
   // Still used by WebIDE, which is going away but not entirely gone.
   {file: "resource://gre/modules/ZipUtils.jsm"},
   // Bug 1494170
   // (The references to these files are dynamically generated, so the test can't
--- a/browser/base/content/test/trackingUI/browser_trackingUI_report_breakage.js
+++ b/browser/base/content/test/trackingUI/browser_trackingUI_report_breakage.js
@@ -260,16 +260,17 @@ async function testReportBreakage(url, t
       let prefs = [
         "privacy.trackingprotection.enabled",
         "privacy.trackingprotection.pbmode.enabled",
         "urlclassifier.trackingTable",
         "network.http.referer.defaultPolicy",
         "network.http.referer.defaultPolicy.pbmode",
         "network.cookie.cookieBehavior",
         "network.cookie.lifetimePolicy",
+        "privacy.annotate_channels.strict_list.enabled",
         "privacy.restrict3rdpartystorage.expiration",
         "privacy.trackingprotection.fingerprinting.enabled",
         "privacy.trackingprotection.cryptomining.enabled",
       ];
       let prefsBody = "";
 
       for (let pref of prefs) {
         prefsBody += `${pref}: ${Preferences.get(pref)}\r\n`;
--- a/browser/branding/official/branding.nsi
+++ b/browser/branding/official/branding.nsi
@@ -40,16 +40,17 @@
 !define PROFILE_CLEANUP_LABEL_HEIGHT "100u"
 !define PROFILE_CLEANUP_LABEL_ALIGN "left"
 !define PROFILE_CLEANUP_CHECKBOX_LEFT "22u"
 !define PROFILE_CLEANUP_CHECKBOX_WIDTH "175u"
 !define PROFILE_CLEANUP_BUTTON_LEFT "22u"
 !define INSTALL_HEADER_TOP "70u"
 !define INSTALL_HEADER_LEFT "22u"
 !define INSTALL_HEADER_WIDTH "180u"
+!define INSTALL_HEADER_HEIGHT "100u"
 !define INSTALL_BODY_LEFT "22u"
 !define INSTALL_BODY_WIDTH "180u"
 !define INSTALL_INSTALLING_TOP "115u"
 !define INSTALL_INSTALLING_LEFT "270u"
 !define INSTALL_INSTALLING_WIDTH "150u"
 !define INSTALL_PROGRESS_BAR_TOP "100u"
 !define INSTALL_PROGRESS_BAR_LEFT "270u"
 !define INSTALL_PROGRESS_BAR_WIDTH "150u"
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -84,16 +84,17 @@ let LEGACY_ACTORS = {
     child: {
       matches: ["about:logins", "about:logins?*"],
       module: "resource:///actors/AboutLoginsChild.jsm",
       events: {
         "AboutLoginsCreateLogin": {wantUntrusted: true},
         "AboutLoginsDeleteLogin": {wantUntrusted: true},
         "AboutLoginsImport": {wantUntrusted: true},
         "AboutLoginsInit": {wantUntrusted: true},
+        "AboutLoginsOpenFAQ": {wantUntrusted: true},
         "AboutLoginsOpenFeedback": {wantUntrusted: true},
         "AboutLoginsOpenPreferences": {wantUntrusted: true},
         "AboutLoginsOpenSite": {wantUntrusted: true},
         "AboutLoginsRecordTelemetryEvent": {wantUntrusted: true},
         "AboutLoginsUpdateLogin": {wantUntrusted: true},
       },
       messages: [
         "AboutLogins:AllLogins",
@@ -578,16 +579,17 @@ const listeners = {
     "webrtc:UpdateGlobalIndicators": ["webrtcUI"],
     "webrtc:UpdatingIndicators": ["webrtcUI"],
   },
 
   mm: {
     "AboutLogins:CreateLogin": ["AboutLoginsParent"],
     "AboutLogins:DeleteLogin": ["AboutLoginsParent"],
     "AboutLogins:Import": ["AboutLoginsParent"],
+    "AboutLogins:OpenFAQ": ["AboutLoginsParent"],
     "AboutLogins:OpenFeedback": ["AboutLoginsParent"],
     "AboutLogins:OpenPreferences": ["AboutLoginsParent"],
     "AboutLogins:OpenSite": ["AboutLoginsParent"],
     "AboutLogins:Subscribe": ["AboutLoginsParent"],
     "AboutLogins:UpdateLogin": ["AboutLoginsParent"],
     "Content:Click": ["ContentClick"],
     "ContentSearch": ["ContentSearch"],
     "FormValidation:ShowPopup": ["FormValidationHandler"],
--- a/browser/components/aboutlogins/AboutLoginsChild.jsm
+++ b/browser/components/aboutlogins/AboutLoginsChild.jsm
@@ -37,24 +37,28 @@ class AboutLoginsChild extends ActorChil
       case "AboutLoginsCreateLogin": {
         this.mm.sendAsyncMessage("AboutLogins:CreateLogin", {login: event.detail});
         break;
       }
       case "AboutLoginsDeleteLogin": {
         this.mm.sendAsyncMessage("AboutLogins:DeleteLogin", {login: event.detail});
         break;
       }
+      case "AboutLoginsImport": {
+        this.mm.sendAsyncMessage("AboutLogins:Import");
+        break;
+      }
+      case "AboutLoginsOpenFAQ": {
+        this.mm.sendAsyncMessage("AboutLogins:OpenFAQ");
+        break;
+      }
       case "AboutLoginsOpenFeedback": {
         this.mm.sendAsyncMessage("AboutLogins:OpenFeedback");
         break;
       }
-      case "AboutLoginsImport": {
-        this.mm.sendAsyncMessage("AboutLogins:Import");
-        break;
-      }
       case "AboutLoginsOpenPreferences": {
         this.mm.sendAsyncMessage("AboutLogins:OpenPreferences");
         break;
       }
       case "AboutLoginsOpenSite": {
         this.mm.sendAsyncMessage("AboutLogins:OpenSite", {login: event.detail});
         break;
       }
--- a/browser/components/aboutlogins/AboutLoginsParent.jsm
+++ b/browser/components/aboutlogins/AboutLoginsParent.jsm
@@ -4,18 +4,16 @@
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["AboutLoginsParent"];
 
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "E10SUtils",
                                "resource://gre/modules/E10SUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "Localization",
-                               "resource://gre/modules/Localization.jsm");
 ChromeUtils.defineModuleGetter(this, "LoginHelper",
                                "resource://gre/modules/LoginHelper.jsm");
 ChromeUtils.defineModuleGetter(this, "MigrationUtils",
                                "resource:///modules/MigrationUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "Services",
                                "resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
@@ -26,19 +24,22 @@ const ABOUT_LOGINS_ORIGIN = "about:login
 const MASTER_PASSWORD_NOTIFICATION_ID = "master-password-login-required";
 
 const PRIVILEGEDABOUT_PROCESS_PREF =
   "browser.tabs.remote.separatePrivilegedContentProcess";
 const PRIVILEGEDABOUT_PROCESS_ENABLED =
   Services.prefs.getBoolPref(PRIVILEGEDABOUT_PROCESS_PREF, false);
 
 
-const FEEDBACK_URL_PREF = "signon.feedbackURL";
+const FEEDBACK_URL_PREF = "signon.management.page.feedbackURL";
 const FEEDBACK_URL = Services.urlFormatter.formatURLPref(FEEDBACK_URL_PREF);
 
+const FAQ_URL_PREF = "signon.management.page.faqURL";
+const FAQ_URL = Services.prefs.getStringPref(FAQ_URL_PREF);
+
 // When the privileged content process is enabled, we expect about:logins
 // to load in it. Otherwise, it's in a normal web content process.
 const EXPECTED_ABOUTLOGINS_REMOTE_TYPE =
   PRIVILEGEDABOUT_PROCESS_ENABLED ? E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE
                                   : E10SUtils.DEFAULT_REMOTE_TYPE;
 
 const isValidLogin = login => {
   return !(login.origin || "").startsWith("chrome://");
@@ -110,16 +111,20 @@ var AboutLoginsParent = {
           Cu.reportError(ex);
         }
         break;
       }
       case "AboutLogins:OpenFeedback": {
         message.target.ownerGlobal.openWebLinkIn(FEEDBACK_URL, "tab", {relatedToCurrent: true});
         break;
       }
+      case "AboutLogins:OpenFAQ": {
+        message.target.ownerGlobal.openWebLinkIn(FAQ_URL, "tab", {relatedToCurrent: true});
+        break;
+      }
       case "AboutLogins:OpenPreferences": {
         message.target.ownerGlobal.openPreferences("privacy-logins");
         break;
       }
       case "AboutLogins:OpenSite": {
         let guid = message.data.login.guid;
         let logins = LoginHelper.searchLoginsWithObject({guid});
         if (logins.length != 1) {
@@ -210,45 +215,46 @@ var AboutLoginsParent = {
       }
       default: {
         break;
       }
     }
   },
 
   async showMasterPasswordLoginNotifications() {
-    if (!this._l10n) {
-      this._l10n = new Localization(["browser/aboutLogins.ftl"]);
-    }
+    for (let subscriber of this._subscriberIterator()) {
+      let MozXULElement = subscriber.ownerGlobal.MozXULElement;
+      MozXULElement.insertFTLIfNeeded("browser/aboutLogins.ftl");
 
-    let messageString = await this._l10n.formatValue("master-password-notification-message");
-    for (let subscriber of this._subscriberIterator()) {
       // If there's already an existing notification bar, don't do anything.
       let {gBrowser} = subscriber.ownerGlobal;
       let browser = subscriber;
       let notificationBox = gBrowser.getNotificationBox(browser);
       let notification = notificationBox.getNotificationWithValue(MASTER_PASSWORD_NOTIFICATION_ID);
       if (notification) {
         continue;
       }
 
       // Configure the notification bar
       let priority = notificationBox.PRIORITY_WARNING_MEDIUM;
       let iconURL = "chrome://browser/skin/login.svg";
-      let reloadLabel = await this._l10n.formatValue("master-password-reload-button-label");
-      let reloadKey = await this._l10n.formatValue("master-password-reload-button-accesskey");
+
+      let doc = subscriber.ownerDocument;
+      let messageFragment = doc.createDocumentFragment();
+      let message = doc.createElement("span");
+      doc.l10n.setAttributes(message, "master-password-notification-message");
+      messageFragment.appendChild(message);
 
       let buttons = [{
-        label: reloadLabel,
-        accessKey: reloadKey,
+        "l10n-id": "master-password-reload-button",
         popup: null,
         callback() { browser.reload(); },
       }];
 
-      notification = notificationBox.appendNotification(messageString, MASTER_PASSWORD_NOTIFICATION_ID,
+      notification = notificationBox.appendNotification(messageFragment, MASTER_PASSWORD_NOTIFICATION_ID,
                                                         iconURL, priority, buttons);
     }
   },
 
   removeMasterPasswordLoginNotifications() {
     for (let subscriber of this._subscriberIterator()) {
       let {gBrowser} = subscriber.ownerGlobal;
       let browser = subscriber;
--- a/browser/components/aboutlogins/content/aboutLogins.css
+++ b/browser/components/aboutlogins/content/aboutLogins.css
@@ -22,17 +22,16 @@ header {
 
 login-filter {
   flex: auto;
   align-self: center;
 }
 
 login-list {
   grid-area: logins;
-  overflow: hidden auto;
 }
 
 login-item {
   grid-area: login;
   max-width: 800px;
 }
 
 #branding-logo {
--- a/browser/components/aboutlogins/content/aboutLogins.ftl
+++ b/browser/components/aboutlogins/content/aboutLogins.ftl
@@ -1,70 +1,81 @@
 # 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 not in a locales directory to prevent it from
 ### being translated as the feature is still in heavy development
 ### and strings are likely to change often.
 
-### Fluent isn't translating elements in the shadow DOM so the translated strings
-### need to be applied to the composed node where they can be moved to the proper
-### descendant after translation.
-
 about-logins-page-title = Logins & Passwords
 
-create-login-button = New Login
-
 login-filter =
   .placeholder = Search Logins
 
+create-login-button = New Login
+
+## The ⋯ menu that is in the top corner of the page
+menu =
+  .title = Open menu
+menu-menuitem-faq = Frequently Asked Questions
+menu-menuitem-feedback = Leave Feedback
+menu-menuitem-import = Import Passwords…
+menu-menuitem-preferences =
+  { PLATFORM() ->
+      [windows] Options
+     *[other] Preferences
+  }
+
+## Login List
 login-list =
-  .count =
-    { $count ->
-        [one] { $count } login
-       *[other] { $count } logins
-    }
-  .last-changed-option = Last Changed
-  .last-used-option = Last Used
-  .missing-username = (no username)
-  .name-option = Name
-  .new-login-subtitle = Enter your login credentials
-  .new-login-title = New Login
-  .sort-label-text = Sort by:
+  .aria-label = Logins matching search query
+login-list-count =
+  { $count ->
+      [one] { $count } login
+     *[other] { $count } logins
+  }
+login-list-last-changed-option = Last Changed
+login-list-last-used-option = Last Used
+login-list-name-option = Name
+login-list-sort-label-text = Sort by:
+login-list-item-title-new-login = New Login
+login-list-item-subtitle-new-login = Enter your login credentials
+login-list-item-subtitle-missing-username = (no username)
 
-login-item =
-  .cancel-button = Cancel
-  .copied-password-button = ✓ Copied!
-  .copied-username-button = ✓ Copied!
-  .copy-password-button = Copy
-  .copy-username-button = Copy
-  .delete-button = Delete
-  .edit-button = Edit
-  .new-login-title = Create New Login
-  .open-site-button = Launch
-  .origin-label = Website Address
-  .origin-placeholder = https://www.example.com
-  .password-hide-title = Hide password
-  .password-label = Password
-  .password-show-title = Show password
-  .save-changes-button = Save Changes
-  .time-created = Created: { DATETIME($timeCreated, day: "numeric", month: "long", year: "numeric") }
-  .time-changed = Last modified: { DATETIME($timeChanged, day: "numeric", month: "long", year: "numeric") }
-  .time-used = Last used: { DATETIME($timeUsed, day: "numeric", month: "long", year: "numeric") }
-  .username-label = Username
-  .username-placeholder = name@example.com
+## Login
+login-item-new-login-title = Create New Login
+login-item-edit-button = Edit
+login-item-delete-button = Delete
+login-item-origin-label = Website Address
+login-item-origin =
+  .placeholder = https://www.example.com
+login-item-open-site-button = Launch
+login-item-username-label = Username
+login-item-username =
+  .placeholder = name@example.com
+login-item-copied-username-button-text = ✔ Copied!
+login-item-copy-username-button-text = Copy
+login-item-password-label = Password
+login-item-password-reveal-checkbox-show =
+  .title = Show password
+login-item-password-reveal-checkbox-hide =
+  .title = Hide password
+login-item-copied-password-button-text = ✔ Copied!
+login-item-copy-password-button-text = Copy
+login-item-save-changes-button = Save Changes
+login-item-cancel-button = Cancel
+login-item-time-changed = Last modified: { DATETIME($timeChanged, day: "numeric", month: "long", year: "numeric") }
+login-item-time-created = Created: { DATETIME($timeCreated, day: "numeric", month: "long", year: "numeric") }
+login-item-time-used = Last used: { DATETIME($timeUsed, day: "numeric", month: "long", year: "numeric") }
 
+## Master Password notification
 master-password-notification-message = Please enter your master password to view saved logins & passwords
-# TODO: Not sure how to use formatValue with these as attributes on a single ID
-master-password-reload-button-label = Log in
-# TODO: Not sure how to use formatValue with these as attributes on a single ID
-master-password-reload-button-accesskey = L
+master-password-reload-button =
+  .label = Log in
+  .accesskey = L
 
-menu-button =
-  .button-title = Open menu
-  .menuitem-feedback = Leave Feedback
-  .menuitem-import = Import Passwords…
-  .menuitem-preferences =
-    { PLATFORM() ->
-        [windows] Options
-       *[other] Preferences
-    }
+confirm-delete-dialog-title = Confirm Deletion
+confirm-delete-dialog-message = Are you sure you want to delete this login?
+confirm-delete-dialog-dismiss-button =
+  .title = Cancel
+confirm-delete-dialog-cancel-button = Cancel
+confirm-delete-dialog-confirm-button = Delete login
--- a/browser/components/aboutlogins/content/aboutLogins.html
+++ b/browser/components/aboutlogins/content/aboutLogins.html
@@ -4,167 +4,148 @@
 
 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy" content="default-src 'none'; object-src 'none'; script-src resource: chrome:; img-src data: blob:;"/>
     <title data-l10n-id="about-logins-page-title"></title>
     <link rel="localization" href="browser/aboutLogins.ftl">
-    <script type="module" src="chrome://browser/content/aboutlogins/components/copy-to-clipboard-button.js"></script>
+    <script type="module" src="chrome://browser/content/aboutlogins/components/confirm-delete-dialog.js"></script>
     <script type="module" src="chrome://browser/content/aboutlogins/components/login-filter.js"></script>
     <script type="module" src="chrome://browser/content/aboutlogins/components/login-item.js"></script>
     <script type="module" src="chrome://browser/content/aboutlogins/components/login-list.js"></script>
     <script type="module" src="chrome://browser/content/aboutlogins/components/login-list-item.js"></script>
     <script type="module" src="chrome://browser/content/aboutlogins/components/menu-button.js"></script>
     <script type="module" src="chrome://browser/content/aboutlogins/aboutLogins.js"></script>
     <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
     <link rel="stylesheet" href="chrome://browser/content/aboutlogins/aboutLogins.css">
     <link rel="icon" href="chrome://browser/content/aboutlogins/icons/favicon.svg">
   </head>
   <body>
     <header>
       <img id="branding-logo" src="chrome://branding/content/aboutlogins.svg" alt=""/>
-      <login-filter data-l10n-id="login-filter"
-                    data-l10n-attrs="placeholder"></login-filter>
+      <login-filter></login-filter>
       <button id="create-login-button" data-l10n-id="create-login-button"></button>
-      <menu-button data-l10n-id="menu-button"
-                   data-l10n-attrs="button-title,
-                                    menuitem-feedback,
-                                    menuitem-import,
-                                    menuitem-preferences"></menu-button>
+      <menu-button></menu-button>
     </header>
-    <login-list data-l10n-id="login-list"
-                data-l10n-args='{"count": 0}'
-                data-l10n-attrs="count,
-                                 last-changed-option,
-                                 last-used-option,
-                                 missing-username,
-                                 name-option,
-                                 new-login-subtitle,
-                                 new-login-title,
-                                 sort-label-text"></login-list>
-    <login-item data-l10n-id="login-item"
-                data-l10n-args='{"timeCreated": 0, "timeChanged": 0, "timeUsed": 0}'
-                data-l10n-attrs="cancel-button,
-                                 copy-password-button,
-                                 copy-username-button,
-                                 copied-password-button,
-                                 copied-username-button,
-                                 delete-button,
-                                 edit-button,
-                                 new-login-title,
-                                 open-site-button,
-                                 origin-label,
-                                 origin-placeholder,
-                                 password-hide-title,
-                                 password-label,
-                                 password-show-title,
-                                 save-changes-button,
-                                 time-created,
-                                 time-changed,
-                                 time-used,
-                                 username-label,
-                                 username-placeholder"></login-item>
+    <login-list></login-list>
+    <login-item></login-item>
+    <confirm-delete-dialog hidden></confirm-delete-dialog>
+
+    <template id="confirm-delete-dialog-template">
+      <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
+      <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/confirm-delete-dialog.css">
+      <div class="overlay">
+        <div class="container" role="dialog" aria-labelledby="title" aria-describedby="message">
+          <div class="title-bar">
+            <h1 class="title" id="title" data-l10n-id="confirm-delete-dialog-title"></h1>
+            <button class="dismiss-button" data-l10n-id="confirm-delete-dialog-dismiss-button"></button>
+          </div>
+          <div class="content">
+            <p class="message" id="message" data-l10n-id="confirm-delete-dialog-message"></p>
+          </div>
+          <div class="buttons">
+            <button class="cancel-button" data-l10n-id="confirm-delete-dialog-cancel-button"></button>
+            <button class="confirm-button" data-l10n-id="confirm-delete-dialog-confirm-button"></button>
+          </div>
+        </div>
+      </div>
+    </template>
 
     <template id="login-list-template">
       <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-list.css">
       <div class="meta">
         <label for="login-sort">
-          <span class="sort-label-text"></span>
+          <span data-l10n-id="login-list-sort-label-text"></span>
           <select id="login-sort">
-            <option class="name-option" value="name"/>
-            <option class="last-used-option" value="last-used"/>
-            <option class="last-changed-option" value="last-changed"/>
+            <option data-l10n-id="login-list-name-option" value="name"/>
+            <option data-l10n-id="login-list-last-used-option" value="last-used"/>
+            <option data-l10n-id="login-list-last-changed-option" value="last-changed"/>
           </select>
         </label>
-        <span class="count"></span>
+        <span class="count" data-l10n-id="login-list-count" data-l10n-args='{"count": 0}'></span>
       </div>
-      <ol>
+      <ol role="listbox" tabindex="0" data-l10n-id="login-list">
       </ol>
     </template>
 
     <template id="login-list-item-template">
       <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-list-item.css">
       <span class="title"></span>
       <span class="username"></span>
     </template>
 
     <template id="login-item-template">
       <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-item.css">
       <div class="header">
-        <h2 class="title"></h2>
-        <button class="edit-button alternate-button"></button>
-        <button class="delete-button alternate-button"></button>
+        <h2 class="title">
+          <span class="login-item-title"></span>
+          <span class="new-login-title" data-l10n-id="login-item-new-login-title"></span>
+        </h2>
+        <button class="edit-button alternate-button" data-l10n-id="login-item-edit-button"></button>
+        <button class="delete-button alternate-button" data-l10n-id="login-item-delete-button"></button>
       </div>
       <form>
         <div class="detail-row">
           <label class="detail-cell">
-            <span class="origin-label field-label"></span>
-            <input type="url" name="origin" required/>
+            <span class="origin-label field-label" data-l10n-id="login-item-origin-label"></span>
+            <input type="url" name="origin" required data-l10n-id="login-item-origin"/>
           </label>
-          <button class="open-site-button"></button>
+          <button class="open-site-button" data-l10n-id="login-item-open-site-button"></button>
         </div>
         <div class="detail-row">
           <label class="detail-cell">
-            <span class="username-label field-label"></span>
-            <input type="text" name="username"/>
+            <span class="username-label field-label" data-l10n-id="login-item-username-label"></span>
+            <input type="text" name="username" data-l10n-id="login-item-username"/>
           </label>
-          <copy-to-clipboard-button class="copy-username-button"
-                                    data-telemetry-object="username"></copy-to-clipboard-button>
+          <button class="copy-button copy-username-button" data-copy-login-property="username" data-telemetry-object="username">
+            <span class="copied-button-text" data-l10n-id="login-item-copied-username-button-text"></span>
+            <span class="copy-button-text" data-l10n-id="login-item-copy-username-button-text"></span>
+          </button>
         </div>
         <div class="detail-row">
           <label class="detail-cell">
-            <span class="password-label field-label"></span>
+            <span class="password-label field-label" data-l10n-id="login-item-password-label"></span>
             <div class="reveal-password-wrapper">
               <input type="password" name="password" required/>
-              <input type="checkbox" class="reveal-password-checkbox"/>
+              <input type="checkbox"
+                     class="reveal-password-checkbox"
+                     data-l10n-id="login-item-password-reveal-checkbox"/>
             </div>
           </label>
-          <copy-to-clipboard-button class="copy-password-button"
-                                    data-telemetry-object="password"></copy-to-clipboard-button>
+          <button class="copy-button copy-password-button" data-copy-login-property="password" data-telemetry-object="password">
+            <span class="copied-button-text" data-l10n-id="login-item-copied-password-button-text"></span>
+            <span class="copy-button-text" data-l10n-id="login-item-copy-password-button-text"></span>
+          </button>
         </div>
-        <p class="time-created meta-info"></p>
-        <p class="time-changed meta-info"></p>
-        <p class="time-used meta-info"></p>
-        <button class="save-changes-button"></button>
-        <button class="cancel-button"></button>
+        <p class="time-created meta-info" data-l10n-id="login-item-time-created" data-l10n-args='{"timeCreated": 0}'></p>
+        <p class="time-changed meta-info" data-l10n-id="login-item-time-changed" data-l10n-args='{"timeChanged": 0}'></p>
+        <p class="time-used meta-info" data-l10n-id="login-item-time-used" data-l10n-args='{"timeUsed": 0}'></p>
+        <button class="save-changes-button" data-l10n-id="login-item-save-changes-button"></button>
+        <button class="cancel-button" data-l10n-id="login-item-cancel-button"></button>
       </form>
     </template>
 
     <template id="login-filter-template">
       <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-filter.css">
-      <input class="filter" type="text"/>
+      <input data-l10n-id="login-filter" class="filter" type="text"/>
     </template>
 
     <template id="menu-button-template">
       <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/common.css">
       <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/menu-button.css">
-      <button class="menu-button alternate-button"></button>
+      <button class="menu-button alternate-button" data-l10n-id="menu"></button>
       <ul class="menu" role="menu" hidden>
-        <li role="menuitem" class="menuitem windows-only">
-          <button class="menuitem-button menuitem-import alternate-button" data-event-name="AboutLoginsImport"></button>
-        </li>
-        <li role="menuitem" class="menuitem">
-          <button class="menuitem-button menuitem-feedback alternate-button" data-event-name="AboutLoginsOpenFeedback"></button>
-        </li>
-        <li role="menuitem" class="menuitem">
-          <button class="menuitem-button menuitem-preferences alternate-button" data-event-name="AboutLoginsOpenPreferences"></button>
-        </li>
+        <button role="menuitem" class="menuitem-button menuitem-import alternate-button" hidden data-supported-platforms="Win32" data-event-name="AboutLoginsImport" data-l10n-id="menu-menuitem-import"></button>
+        <button role="menuitem" class="menuitem-button menuitem-preferences alternate-button" data-event-name="AboutLoginsOpenPreferences" data-l10n-id="menu-menuitem-preferences"></button>
+        <button role="menuitem" class="menuitem-button menuitem-feedback alternate-button" data-event-name="AboutLoginsOpenFeedback" data-l10n-id="menu-menuitem-feedback"></button>
+        <button role="menuitem" class="menuitem-button menuitem-faq alternate-button" data-event-name="AboutLoginsOpenFAQ" data-l10n-id="menu-menuitem-faq"></button>
       </ul>
     </template>
-
-    <template id="copy-to-clipboard-button-template">
-      <link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
-      <link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/copy-to-clipboard-button.css">
-      <button class="copy-button">
-        <span class="copied-button-text"></span>
-        <span class="copy-button-text"></span>
-      </button>
-    </template>
   </body>
 </html>
--- a/browser/components/aboutlogins/content/common.css
+++ b/browser/components/aboutlogins/content/common.css
@@ -1,12 +1,18 @@
 /* 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/. */
 
+/* [hidden] isn't applying to elements in Shadow DOM. */
+:host([hidden]),
+[hidden] {
+  display: none !important;
+}
+
 .alternate-button {
   background-color: transparent;
 }
 
 .alternate-button:hover {
   background-color: var(--in-content-button-background-hover);
 }
 
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/confirm-delete-dialog.css
@@ -0,0 +1,94 @@
+:host {
+  /* these variable values come from about:preferences */
+  --in-content-dialogtitle-background: #f1f1f1;
+  --in-content-dialogtitle-border: #c1c1c1;
+}
+
+.overlay {
+  position: fixed;
+  z-index: 1;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  /* TODO: this color is used in the about:preferences overlay, but
+           why isn't it declared as a variable? */
+  background-color: rgba(0,0,0,0.5);
+  display: flex;
+  align-items: center;
+}
+
+.container {
+  z-index: 2;
+
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+
+  width: 50%;
+  min-width: 250px;
+  max-width: 500px;
+  height: 40%;
+  min-height: 200px;
+  margin: auto;
+  background: var(--in-content-page-background);
+  color: var(--in-content-page-color);
+}
+
+.title-bar {
+  position: relative;
+  flex: 0 1 auto;
+  text-align: center;
+  background-color: var(--in-content-dialogtitle-background);
+  padding: 5px;
+  border-bottom: 1px solid var(--in-content-dialogtitle-border);
+}
+
+.title {
+  font-size: .9em;
+  line-height: 1.8em;
+  font-weight: 600;
+  -moz-user-select: none;
+  margin: 0;
+}
+
+button.dismiss-button {
+  position: absolute;
+  top: 0;
+  right: 0;
+  min-width: 20px;
+  min-height: 20px;
+  margin: 8px 16px;
+  padding: 0;
+  background: url(chrome://global/skin/icons/close.svg) no-repeat center;
+  -moz-context-properties: fill, fill-opacity;
+  fill: currentColor;
+  fill-opacity: 0;
+}
+
+button.dismiss-button:dir(rtl) {
+  right: auto;
+  left: 0;
+}
+
+.content {
+  flex: 1 1 auto;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.buttons {
+  flex: 0 1 auto;
+  display: flex;
+  justify-content: space-between;
+}
+
+.buttons button {
+  margin: 0;
+}
+
+.content,
+.buttons {
+  margin: 16px;
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/confirm-delete-dialog.js
@@ -0,0 +1,87 @@
+/* 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/. */
+
+export default class ConfirmDeleteDialog extends HTMLElement {
+  constructor() {
+    super();
+    this._promise = null;
+  }
+
+  connectedCallback() {
+    if (this.shadowRoot) {
+      return;
+    }
+    let template = document.querySelector("#confirm-delete-dialog-template");
+    let shadowRoot = this.attachShadow({mode: "open"});
+    document.l10n.connectRoot(shadowRoot);
+    shadowRoot.appendChild(template.content.cloneNode(true));
+
+    this._cancelButton = this.shadowRoot.querySelector(".cancel-button");
+    this._confirmButton = this.shadowRoot.querySelector(".confirm-button");
+    this._dismissButton = this.shadowRoot.querySelector(".dismiss-button");
+    this._message = this.shadowRoot.querySelector(".message");
+    this._overlay = this.shadowRoot.querySelector(".overlay");
+    this._title = this.shadowRoot.querySelector(".title");
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "keydown":
+        if (event.key === "Escape" && !event.defaultPrevented) {
+          this.onCancel();
+        }
+        break;
+      case "click":
+        if (event.target.classList.contains("cancel-button") ||
+            event.target.classList.contains("dismiss-button") ||
+            event.target.classList.contains("overlay")) {
+          this.onCancel();
+        } else if (event.target.classList.contains("confirm-button")) {
+          this.onConfirm();
+        }
+    }
+  }
+
+  hide() {
+    this._cancelButton.removeEventListener("click", this);
+    this._confirmButton.removeEventListener("click", this);
+    this._dismissButton.removeEventListener("click", this);
+    this._overlay.removeEventListener("click", this);
+    window.removeEventListener("keydown", this);
+
+    this.hidden = true;
+  }
+
+  show() {
+    this.hidden = false;
+
+    this._cancelButton.addEventListener("click", this);
+    this._confirmButton.addEventListener("click", this);
+    this._dismissButton.addEventListener("click", this);
+    this._overlay.addEventListener("click", this);
+    window.addEventListener("keydown", this);
+
+    // For accessibility, focus the least destructive action button when the
+    // dialog loads.
+    this._cancelButton.focus();
+
+    this._promise = new Promise((resolve, reject) => {
+      this._resolve = resolve;
+      this._reject = reject;
+    });
+
+    return this._promise;
+  }
+
+  onCancel() {
+    this._reject();
+    this.hide();
+  }
+
+  onConfirm() {
+    this._resolve();
+    this.hide();
+  }
+}
+customElements.define("confirm-delete-dialog", ConfirmDeleteDialog);
deleted file mode 100644
--- a/browser/components/aboutlogins/content/components/copy-to-clipboard-button.css
+++ /dev/null
@@ -1,29 +0,0 @@
-/* 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/. */
-
-:host {
-  --success-color: #00c100;
-}
-
-@supports -moz-bool-pref("browser.in-content.dark-mode") {
-@media (prefers-color-scheme: dark) {
-  :host {
-    --success-color: #86DE74;
-  }
-}
-}
-
-:host(:not([data-copied])) .copied-button-text,
-:host([data-copied]) .copy-button-text {
-  display: none;
-}
-
-:host([data-copied]) {
-  color: var(--success-color);
-}
-
-:host([data-copied]) button {
-  background-color: transparent;
-  opacity: 1; /* override common.css fading out disabled buttons */
-}
deleted file mode 100644
--- a/browser/components/aboutlogins/content/components/copy-to-clipboard-button.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/* 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 {recordTelemetryEvent} from "../aboutLoginsUtils.js";
-import ReflectedFluentElement from "./reflected-fluent-element.js";
-
-export default class CopyToClipboardButton extends ReflectedFluentElement {
-  /**
-   * The number of milliseconds to display the "Copied" success message
-   * before reverting to the normal "Copy" button.
-   */
-  static get BUTTON_RESET_TIMEOUT() {
-    return 5000;
-  }
-
-  constructor() {
-    super();
-
-    this._relatedInput = null;
-  }
-
-  connectedCallback() {
-    if (this.shadowRoot) {
-      return;
-    }
-
-    let CopyToClipboardButtonTemplate = document.querySelector("#copy-to-clipboard-button-template");
-    this.attachShadow({mode: "open"})
-        .appendChild(CopyToClipboardButtonTemplate.content.cloneNode(true));
-
-    this._copyButton = this.shadowRoot.querySelector(".copy-button");
-    this._copyButton.addEventListener("click", this);
-
-    super.connectedCallback();
-  }
-
-  static get reflectedFluentIDs() {
-    return ["copy-button-text", "copied-button-text"];
-  }
-
-  static get observedAttributes() {
-    return CopyToClipboardButton.reflectedFluentIDs;
-  }
-
-  handleSpecialCaseFluentString(attrName) {
-    if (attrName != "copied-button-text" &&
-        attrName != "copy-button-text") {
-      return false;
-    }
-
-    let span = this.shadowRoot.querySelector("." + attrName);
-    span.textContent = this.getAttribute(attrName);
-    return true;
-  }
-
-  handleEvent(event) {
-    if (event.type != "click" || event.currentTarget != this._copyButton) {
-      return;
-    }
-
-    this._copyButton.disabled = true;
-    navigator.clipboard.writeText(this._relatedInput.value).then(() => {
-      this.dataset.copied = true;
-      setTimeout(() => {
-        this._copyButton.disabled = false;
-        delete this.dataset.copied;
-      }, CopyToClipboardButton.BUTTON_RESET_TIMEOUT);
-    }, () => this._copyButton.disabled = false);
-
-    if (this.dataset.telemetryObject) {
-      recordTelemetryEvent({object: this.dataset.telemetryObject, method: "copy"});
-    }
-  }
-
-  /**
-   * @param {Element} val A reference to the input element whose value will
-   *                      be placed on the clipboard.
-   */
-  set relatedInput(val) {
-    this._relatedInput = val;
-  }
-}
-customElements.define("copy-to-clipboard-button", CopyToClipboardButton);
--- a/browser/components/aboutlogins/content/components/login-filter.js
+++ b/browser/components/aboutlogins/content/components/login-filter.js
@@ -1,68 +1,48 @@
 /* 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 {recordTelemetryEvent} from "../aboutLoginsUtils.js";
-import ReflectedFluentElement from "./reflected-fluent-element.js";
 
-export default class LoginFilter extends ReflectedFluentElement {
+export default class LoginFilter extends HTMLElement {
   connectedCallback() {
     if (this.shadowRoot) {
       return;
     }
 
     let loginFilterTemplate = document.querySelector("#login-filter-template");
-    this.attachShadow({mode: "open"})
-        .appendChild(loginFilterTemplate.content.cloneNode(true));
+    let shadowRoot = this.attachShadow({mode: "open"});
+    document.l10n.connectRoot(shadowRoot);
+    shadowRoot.appendChild(loginFilterTemplate.content.cloneNode(true));
 
     this._input = this.shadowRoot.querySelector("input");
 
     this.addEventListener("input", this);
-
-    super.connectedCallback();
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "input": {
         this._dispatchFilterEvent(event.originalTarget.value);
         break;
       }
     }
   }
 
-  static get reflectedFluentIDs() {
-    return ["placeholder"];
-  }
-
-  static get observedAttributes() {
-    return this.reflectedFluentIDs;
-  }
-
   get value() {
     return this._input.value;
   }
 
   set value(val) {
     this._input.value = val;
     this._dispatchFilterEvent(val);
   }
 
-  handleSpecialCaseFluentString(attrName) {
-    if (!this.shadowRoot ||
-        attrName != "placeholder") {
-      return false;
-    }
-
-    this._input.placeholder = this.getAttribute(attrName);
-    return true;
-  }
-
   _dispatchFilterEvent(value) {
     this.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
       bubbles: true,
       composed: true,
       detail: value,
     }));
 
     recordTelemetryEvent({object: "list", method: "filter"});
--- a/browser/components/aboutlogins/content/components/login-item.css
+++ b/browser/components/aboutlogins/content/components/login-item.css
@@ -3,24 +3,35 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 :host {
   padding: 18px;
 
   --reveal-checkbox-opacity: .8;
   --reveal-checkbox-opacity-hover: .6;
   --reveal-checkbox-opacity-active: 1;
+  --success-color: #00c100;
+}
+
+@supports -moz-bool-pref("browser.in-content.dark-mode") {
+@media (prefers-color-scheme: dark) {
+  :host {
+    --success-color: #86DE74;
+  }
+}
 }
 
 :host([data-editing]) .edit-button,
 :host([data-is-new-login]) .delete-button,
 :host([data-is-new-login]) .origin-saved-value,
 :host([data-is-new-login]) copy-to-clipboard-button,
 :host([data-is-new-login]) .open-site-button,
 :host([data-is-new-login]) .meta-info,
+:host([data-is-new-login]) .login-item-title,
+:host(:not([data-is-new-login])) .new-login-title,
 :host(:not([data-editing])) .cancel-button,
 :host(:not([data-editing])) .save-changes-button {
   display: none;
 }
 
 :host(:not([data-editing])) input[type="password"],
 :host(:not([data-editing])) input[type="text"],
 :host(:not([data-editing])) input[type="url"] {
@@ -85,16 +96,27 @@
 
 .field-label {
   display: block;
   font-size: smaller;
   color: var(--in-content-deemphasized-text);
   margin-bottom: 5px;
 }
 
+.copy-button:not([data-copied]) .copied-button-text,
+.copy-button[data-copied] .copy-button-text {
+  display: none;
+}
+
+.copy-button[data-copied] {
+  color: var(--success-color) !important; /* override common.css */
+  background-color: transparent;
+  opacity: 1; /* override common.css fading out disabled buttons */
+}
+
 .meta-info {
   font-size: smaller;
 }
 
 .meta-info:first-of-type {
   padding-top: 1em;
   border-top: 1px solid var(--in-content-box-border-color);
   width: 40px;
@@ -125,16 +147,26 @@
 .reveal-password-checkbox:hover:active {
   opacity: var(--reveal-checkbox-opacity-active);
 }
 
 .reveal-password-checkbox:checked {
   background-image: url("chrome://browser/content/aboutlogins/icons/hide-password.svg") !important;
 }
 
+.reveal-password-checkbox:-moz-focusring {
+  outline: 2px solid var(--in-content-border-active);
+  /* offset outline to align with 1px border-width set for buttons/menulists above. */
+  outline-offset: -1px;
+  /* Make outline-radius slightly bigger than the border-radius set above,
+   * to make the thicker outline corners look smooth */
+  -moz-outline-radius: 3px;
+  box-shadow: 0 0 0 4px var(--in-content-border-active-shadow);
+}
+
 @supports -moz-bool-pref("browser.in-content.dark-mode") {
 @media (prefers-color-scheme: dark) {
   :host {
     --reveal-checkbox-opacity: .8;
     --reveal-checkbox-opacity-hover: 1;
     --reveal-checkbox-opacity-active: .6;
   }
 }
--- a/browser/components/aboutlogins/content/components/login-item.js
+++ b/browser/components/aboutlogins/content/components/login-item.js
@@ -1,143 +1,80 @@
 /* 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 {recordTelemetryEvent} from "../aboutLoginsUtils.js";
-import ReflectedFluentElement from "./reflected-fluent-element.js";
 
-export default class LoginItem extends ReflectedFluentElement {
+export default class LoginItem extends HTMLElement {
+  /**
+   * The number of milliseconds to display the "Copied" success message
+   * before reverting to the normal "Copy" button.
+   */
+  static get COPY_BUTTON_RESET_TIMEOUT() {
+    return 5000;
+  }
+
   constructor() {
     super();
     this._login = {};
   }
 
   connectedCallback() {
     if (this.shadowRoot) {
       this.render();
       return;
     }
 
     let loginItemTemplate = document.querySelector("#login-item-template");
-    this.attachShadow({mode: "open"})
-        .appendChild(loginItemTemplate.content.cloneNode(true));
+    let shadowRoot = this.attachShadow({mode: "open"});
+    document.l10n.connectRoot(shadowRoot);
+    shadowRoot.appendChild(loginItemTemplate.content.cloneNode(true));
 
     for (let selector of [
+      ".copy-password-button",
+      ".copy-username-button",
       ".delete-button",
       ".edit-button",
       ".open-site-button",
+      ".reveal-password-checkbox",
       ".save-changes-button",
       ".cancel-button",
     ]) {
       let button = this.shadowRoot.querySelector(selector);
       button.addEventListener("click", this);
     }
 
+    this._confirmDeleteDialog = document.querySelector("confirm-delete-dialog");
     this._copyPasswordButton = this.shadowRoot.querySelector(".copy-password-button");
     this._copyUsernameButton = this.shadowRoot.querySelector(".copy-username-button");
     this._deleteButton = this.shadowRoot.querySelector(".delete-button");
     this._editButton = this.shadowRoot.querySelector(".edit-button");
     this._form = this.shadowRoot.querySelector("form");
     this._originInput = this.shadowRoot.querySelector("input[name='origin']");
     this._usernameInput = this.shadowRoot.querySelector("input[name='username']");
     this._passwordInput = this.shadowRoot.querySelector("input[name='password']");
     this._revealCheckbox = this.shadowRoot.querySelector(".reveal-password-checkbox");
-    this._title = this.shadowRoot.querySelector(".title");
-
-    this._copyUsernameButton.relatedInput = this._usernameInput;
-    this._copyPasswordButton.relatedInput = this._passwordInput;
+    this._title = this.shadowRoot.querySelector(".login-item-title");
+    this._timeCreated = this.shadowRoot.querySelector(".time-created");
+    this._timeChanged = this.shadowRoot.querySelector(".time-changed");
+    this._timeUsed = this.shadowRoot.querySelector(".time-used");
 
     this.render();
 
     this._originInput.addEventListener("blur", this);
-    this._revealCheckbox.addEventListener("click", this);
     window.addEventListener("AboutLoginsLoginSelected", this);
-
-    super.connectedCallback();
-  }
-
-  static get reflectedFluentIDs() {
-    return [
-      "cancel-button",
-      "copied-password-button",
-      "copied-username-button",
-      "copy-password-button",
-      "copy-username-button",
-      "delete-button",
-      "edit-button",
-      "new-login-title",
-      "open-site-button",
-      "origin-label",
-      "origin-placeholder",
-      "password-hide-title",
-      "password-label",
-      "password-show-title",
-      "save-changes-button",
-      "time-created",
-      "time-changed",
-      "time-used",
-      "username-label",
-      "username-placeholder",
-    ];
-  }
-
-  static get observedAttributes() {
-    return this.reflectedFluentIDs;
-  }
-
-  handleSpecialCaseFluentString(attrName) {
-    switch (attrName) {
-      case "copied-password-button":
-      case "copy-password-button": {
-        let newAttrName = attrName.substr(0, attrName.indexOf("-")) + "-button-text";
-        this._copyPasswordButton.setAttribute(newAttrName, this.getAttribute(attrName));
-        break;
-      }
-      case "copied-username-button":
-      case "copy-username-button": {
-        let newAttrName = attrName.substr(0, attrName.indexOf("-")) + "-button-text";
-        this._copyUsernameButton.setAttribute(newAttrName, this.getAttribute(attrName));
-        break;
-      }
-      case "new-login-title": {
-        this._title.setAttribute(attrName, this.getAttribute(attrName));
-        if (!this._login.title) {
-          this._title.textContent = this.getAttribute(attrName);
-        }
-        break;
-      }
-      case "origin-placeholder": {
-        this._originInput.setAttribute("placeholder", this.getAttribute(attrName));
-        break;
-      }
-      case "password-hide-title":
-      case "password-show-title": {
-        this._updatePasswordRevealState();
-        break;
-      }
-      case "username-placeholder": {
-        this._usernameInput.setAttribute("placeholder", this.getAttribute(attrName));
-        break;
-      }
-      default:
-        return false;
-    }
-    return true;
   }
 
   render() {
-    let l10nArgs = {
-      timeCreated: this._login.timeCreated || "",
-      timeChanged: this._login.timePasswordChanged || "",
-      timeUsed: this._login.timeLastUsed || "",
-    };
-    document.l10n.setAttributes(this, "login-item", l10nArgs);
+    document.l10n.setAttributes(this._timeCreated, "login-item-time-created", {timeCreated: this._login.timeCreated || ""});
+    document.l10n.setAttributes(this._timeChanged, "login-item-time-changed", {timeChanged: this._login.timePasswordChanged || ""});
+    document.l10n.setAttributes(this._timeUsed, "login-item-time-used", {timeUsed: this._login.timeLastUsed || ""});
 
-    this._title.textContent = this._login.title || this._title.getAttribute("new-login-title");
+    this._title.textContent = this._login.title;
     this._originInput.defaultValue = this._login.origin || "";
     this._usernameInput.defaultValue = this._login.username || "";
     this._passwordInput.defaultValue = this._login.password || "";
     this._updatePasswordRevealState();
   }
 
   handleEvent(event) {
     switch (event.type) {
@@ -152,17 +89,17 @@ export default class LoginItem extends R
           return;
         }
         if (!originValue.match(/:\/\//)) {
           this._originInput.value = "https://" + originValue;
         }
         break;
       }
       case "click": {
-        let classList = event.target.classList;
+        let classList = event.currentTarget.classList;
         if (classList.contains("reveal-password-checkbox")) {
           this._updatePasswordRevealState();
 
           let method = this._revealCheckbox.checked ? "show" : "hide";
           recordTelemetryEvent({object: "password", method});
           return;
         }
 
@@ -179,23 +116,34 @@ export default class LoginItem extends R
           }
 
           recordTelemetryEvent({
             object: this._login.guid ? "existing_login" : "new_login",
             method: "cancel",
           });
           return;
         }
+        if (classList.contains("copy-password-button") ||
+            classList.contains("copy-username-button")) {
+          let copyButton = event.currentTarget;
+          copyButton.disabled = true;
+          let propertyToCopy = copyButton.dataset.copyLoginProperty;
+          navigator.clipboard.writeText(this._login[propertyToCopy]).then(() => {
+            copyButton.dataset.copied = true;
+            setTimeout(() => {
+              copyButton.disabled = false;
+              delete copyButton.dataset.copied;
+            }, LoginItem.COPY_BUTTON_RESET_TIMEOUT);
+          }, () => copyButton.disabled = false);
+
+          recordTelemetryEvent({object: copyButton.dataset.telemetryObject, method: "copy"});
+          return;
+        }
         if (classList.contains("delete-button")) {
-          document.dispatchEvent(new CustomEvent("AboutLoginsDeleteLogin", {
-            bubbles: true,
-            detail: this._login,
-          }));
-
-          recordTelemetryEvent({object: "existing_login", method: "delete"});
+          this.confirmDelete();
           return;
         }
         if (classList.contains("edit-button")) {
           this._toggleEditing();
 
           recordTelemetryEvent({object: "existing_login", method: "edit"});
           return;
         }
@@ -231,16 +179,31 @@ export default class LoginItem extends R
           }
         }
         break;
       }
     }
   }
 
   /**
+   * Toggles the confirm delete dialog, completing the deletion if the user
+   * agrees.
+   */
+  confirmDelete() {
+    const dialog = document.querySelector("confirm-delete-dialog");
+    dialog.show().then(() => {
+      document.dispatchEvent(new CustomEvent("AboutLoginsDeleteLogin", {
+        bubbles: true,
+        detail: this._login,
+      }));
+      recordTelemetryEvent({object: "existing_login", method: "delete"});
+    }, () => {});
+  }
+
+  /**
    * @param {login} login The login that should be displayed. The login object is
    *                      a plain JS object representation of nsILoginInfo/nsILoginMetaInfo.
    */
   setLogin(login) {
     this._login = login;
 
     this._form.reset();
 
@@ -248,16 +211,17 @@ export default class LoginItem extends R
       delete this.dataset.isNewLogin;
     } else {
       this.dataset.isNewLogin = true;
     }
     this._toggleEditing(!login.guid);
 
     this._revealCheckbox.checked = false;
 
+    this._editButton.focus();
     this.render();
   }
 
   /**
    * Updates the view if the login argument matches the login currently
    * displayed.
    *
    * @param {login} login The login that was added to storage. The login object is
@@ -359,30 +323,34 @@ export default class LoginItem extends R
     } else {
       // Need to set a shorter width than -moz-available so the reveal checkbox
       // will still appear next to the password.
       this._passwordInput.style.width = (this._login.password || "").length + "ch";
     }
 
     this._deleteButton.disabled = this.dataset.isNewLogin;
     this._editButton.disabled = shouldEdit;
-    this._originInput.readOnly = !this.dataset.isNewLogin;
+    let inputTabIndex = !shouldEdit ? -1 : 0;
+    this._originInput.readOnly = !shouldEdit;
+    this._originInput.tabIndex = inputTabIndex;
     this._usernameInput.readOnly = !shouldEdit;
+    this._usernameInput.tabIndex = inputTabIndex;
     this._passwordInput.readOnly = !shouldEdit;
+    this._passwordInput.tabIndex = inputTabIndex;
     if (shouldEdit) {
       this.dataset.editing = true;
+      this._originInput.focus();
     } else {
       delete this.dataset.editing;
     }
   }
 
   _updatePasswordRevealState() {
-    let labelAttr = this._revealCheckbox.checked ? "password-show-title"
-                                                 : "password-hide-title";
-    this._revealCheckbox.setAttribute("aria-label", this.getAttribute(labelAttr));
-    this._revealCheckbox.setAttribute("title", this.getAttribute(labelAttr));
+    let titleId = this._revealCheckbox.checked ? "login-item-password-reveal-checkbox-hide"
+                                               : "login-item-password-reveal-checkbox-show";
+    document.l10n.setAttributes(this._revealCheckbox, titleId);
 
     let {checked} = this._revealCheckbox;
-    let inputType = checked ? "type" : "password";
+    let inputType = checked ? "text" : "password";
     this._passwordInput.setAttribute("type", inputType);
   }
 }
 customElements.define("login-item", LoginItem);
--- a/browser/components/aboutlogins/content/components/login-list-item.css
+++ b/browser/components/aboutlogins/content/components/login-list-item.css
@@ -6,29 +6,29 @@
   display: block;
   padding: 10px;
   padding-inline-end: 18px;
   padding-inline-start: 14px;
   border-inline-start: 4px solid transparent;
   border-bottom: 1px solid var(--in-content-box-border-color);
 }
 
-/* [hidden] isn't applying to elements in Shadow DOM. */
-:host([hidden]) {
-  display: none;
-}
-
 :host(:hover) {
   background-color: var(--in-content-box-background-hover);
 }
 
 :host(:hover:active) {
   background-color: var(--in-content-box-background-active);
 }
 
+:host(.keyboard-selected) {
+  border-inline-start-color: var(--in-content-border-active-shadow);
+  background-color: var(--in-content-box-background-odd);
+}
+
 :host(.selected) {
   border-inline-start-color: var(--in-content-border-highlight);
   background-color: var(--in-content-box-background-hover);
 }
 
 .title {
   font-weight: bold;
 }
--- a/browser/components/aboutlogins/content/components/login-list-item.js
+++ b/browser/components/aboutlogins/content/components/login-list-item.js
@@ -3,47 +3,58 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 import {recordTelemetryEvent} from "../aboutLoginsUtils.js";
 
 export default class LoginListItem extends HTMLElement {
   constructor(login) {
     super();
     this._login = login;
+    this.id = login.guid ?
+      // Prepend the ID with a string since IDs must not begin with a number.
+      "lli-" + this._login.guid :
+      "new-login-list-item";
   }
 
   connectedCallback() {
     if (this.shadowRoot) {
       this.render();
       return;
     }
 
     let loginListItemTemplate = document.querySelector("#login-list-item-template");
-    this.attachShadow({mode: "open"})
-        .appendChild(loginListItemTemplate.content.cloneNode(true));
+    let shadowRoot = this.attachShadow({mode: "open"});
+    document.l10n.connectRoot(shadowRoot);
+    shadowRoot.appendChild(loginListItemTemplate.content.cloneNode(true));
 
     this._title = this.shadowRoot.querySelector(".title");
     this._username = this.shadowRoot.querySelector(".username");
+    this.setAttribute("role", "option");
+
+    this.addEventListener("click", this);
 
     this.render();
-
-    this.addEventListener("click", this);
   }
 
   render() {
     if (!this._login.guid) {
       delete this.dataset.guid;
-      this._title.textContent = this.getAttribute("new-login-title");
-      this._username.textContent = this.getAttribute("new-login-subtitle");
+      document.l10n.setAttributes(this._title, "login-list-item-title-new-login");
+      document.l10n.setAttributes(this._username, "login-list-item-subtitle-new-login");
       return;
     }
 
     this.dataset.guid = this._login.guid;
     this._title.textContent = this._login.title;
-    this._username.textContent = this._login.username.trim() || this.getAttribute("missing-username");
+    if (this._login.username.trim()) {
+      this._username.removeAttribute("data-l10n-id");
+      this._username.textContent = this._login.username.trim();
+    } else {
+      document.l10n.setAttributes(this._username, "login-list-item-subtitle-missing-username");
+    }
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "click": {
         this.dispatchEvent(new CustomEvent("AboutLoginsLoginSelected", {
           bubbles: true,
           composed: true,
--- a/browser/components/aboutlogins/content/components/login-list.css
+++ b/browser/components/aboutlogins/content/components/login-list.css
@@ -11,18 +11,16 @@
 }
 
 .meta {
   display: flex;
   align-items: center;
   padding: 10px 18px;
   border-bottom: 1px solid var(--in-content-box-border-color);
   background-color: var(--in-content-box-info-background);
-  position: sticky;
-  top: 0;
 }
 
 .count {
   flex: auto;
   text-align: end;
   font-size: smaller;
   margin-inline-start: 18px;
 }
--- a/browser/components/aboutlogins/content/components/login-list.js
+++ b/browser/components/aboutlogins/content/components/login-list.js
@@ -1,75 +1,78 @@
 /* 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 LoginListItem from "./login-list-item.js";
-import ReflectedFluentElement from "./reflected-fluent-element.js";
 
 const collator = new Intl.Collator();
 const sortFnOptions = {
   name: (a, b) => collator.compare(a.title, b.title),
   "last-used": (a, b) => (a.timeLastUsed < b.timeLastUsed),
   "last-changed": (a, b) => (a.timePasswordChanged < b.timePasswordChanged),
 };
 
-export default class LoginList extends ReflectedFluentElement {
+export default class LoginList extends HTMLElement {
   constructor() {
     super();
     this._logins = [];
     this._filter = "";
     this._selectedGuid = null;
     this._blankLoginListItem = new LoginListItem({});
   }
 
   connectedCallback() {
     if (this.shadowRoot) {
       return;
     }
     let loginListTemplate = document.querySelector("#login-list-template");
-    this.attachShadow({mode: "open"})
-        .appendChild(loginListTemplate.content.cloneNode(true));
+    let shadowRoot = this.attachShadow({mode: "open"});
+    document.l10n.connectRoot(shadowRoot);
+    shadowRoot.appendChild(loginListTemplate.content.cloneNode(true));
 
     this._list = this.shadowRoot.querySelector("ol");
+    this._count = this.shadowRoot.querySelector(".count");
 
     this.render();
 
     this.shadowRoot.getElementById("login-sort")
                    .addEventListener("change", this);
     window.addEventListener("AboutLoginsLoginSelected", this);
     window.addEventListener("AboutLoginsFilterLogins", this);
-
-    super.connectedCallback();
+    this.addEventListener("keydown", this);
   }
 
   render() {
     this._list.textContent = "";
 
     if (!this._logins.length) {
-      document.l10n.setAttributes(this, "login-list", {count: 0});
+      document.l10n.setAttributes(this._count, "login-list-count", {count: 0});
       return;
     }
 
     if (!this._selectedGuid) {
       this._blankLoginListItem.classList.add("selected");
+      this._blankLoginListItem.setAttribute("aria-selected", "true");
+      this._list.setAttribute("aria-activedescendant", this._blankLoginListItem.id);
       this._list.append(this._blankLoginListItem);
     }
 
     for (let login of this._logins) {
       let listItem = new LoginListItem(login);
-      listItem.setAttribute("missing-username", this.getAttribute("missing-username"));
       if (login.guid == this._selectedGuid) {
         listItem.classList.add("selected");
+        listItem.setAttribute("aria-selected", "true");
+        this._list.setAttribute("aria-activedescendant", listItem.id);
       }
       this._list.append(listItem);
     }
 
     let visibleLoginCount = this._applyFilter();
-    document.l10n.setAttributes(this, "login-list", {count: visibleLoginCount});
+    document.l10n.setAttributes(this._count, "login-list-count", {count: visibleLoginCount});
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "change": {
         const sort = event.target.value;
         this._logins = this._logins.sort((a, b) => sortFnOptions[sort](a, b));
         this.render();
@@ -84,48 +87,21 @@ export default class LoginList extends R
         if (this._selectedGuid == event.detail.guid) {
           return;
         }
 
         this._selectedGuid = event.detail.guid || null;
         this.render();
         break;
       }
-    }
-  }
-
-  static get reflectedFluentIDs() {
-    return ["count",
-            "last-used-option",
-            "last-changed-option",
-            "missing-username",
-            "name-option",
-            "new-login-subtitle",
-            "new-login-title",
-            "sort-label-text"];
-  }
-
-  static get observedAttributes() {
-    return this.reflectedFluentIDs;
-  }
-
-  handleSpecialCaseFluentString(attrName) {
-    switch (attrName) {
-      case "missing-username": {
+      case "keydown": {
+        this._handleKeyboardNav(event);
         break;
       }
-      case "new-login-subtitle":
-      case "new-login-title": {
-        this._blankLoginListItem.setAttribute(attrName, this.getAttribute(attrName));
-        break;
-      }
-      default:
-        return false;
     }
-    return true;
   }
 
   /**
    * @param {login[]} logins An array of logins used for displaying in the list.
    */
   setLogins(logins) {
     this._logins = logins;
     this.render();
@@ -189,10 +165,89 @@ export default class LoginList extends R
         }
       } else if (!listItem.hidden) {
         listItem.hidden = true;
       }
     }
 
     return matchingLoginGuids.length;
   }
+
+  _handleKeyboardNav(event) {
+    if (this._list != this.shadowRoot.activeElement) {
+      return;
+    }
+
+    let isLTR = document.dir == "ltr";
+    let activeDescendantId = this._list.getAttribute("aria-activedescendant");
+    let activeDescendant = activeDescendantId ?
+      this.shadowRoot.getElementById(activeDescendantId) :
+      this._list.firstElementChild;
+    let newlyFocusedItem = null;
+    switch (event.key) {
+      case "ArrowDown": {
+        let nextItem = activeDescendant.nextElementSibling;
+        if (!nextItem) {
+          return;
+        }
+        newlyFocusedItem = nextItem;
+        break;
+      }
+      case "ArrowLeft": {
+        let item = isLTR ?
+          activeDescendant.previousElementSibling :
+          activeDescendant.nextElementSibling;
+        if (!item) {
+          return;
+        }
+        newlyFocusedItem = item;
+        break;
+      }
+      case "ArrowRight": {
+        let item = isLTR ?
+          activeDescendant.nextElementSibling :
+          activeDescendant.previousElementSibling;
+        if (!item) {
+          return;
+        }
+        newlyFocusedItem = item;
+        break;
+      }
+      case "ArrowUp": {
+        let previousItem = activeDescendant.previousElementSibling;
+        if (!previousItem) {
+          return;
+        }
+        newlyFocusedItem = previousItem;
+        break;
+      }
+      case "Tab": {
+        // Bug 1562716: Pressing Tab from the login-list cycles back to the
+        // login-sort dropdown due to the login-list having `overflow`
+        // CSS property set. Explicitly forward focus here until
+        // this keyboard trap is fixed.
+        if (event.shiftKey) {
+          return;
+        }
+        let loginItem = document.querySelector("login-item");
+        if (loginItem) {
+          event.preventDefault();
+          loginItem.shadowRoot.querySelector(".edit-button").focus();
+        }
+        return;
+      }
+      case " ":
+      case "Enter": {
+        event.preventDefault();
+        activeDescendant.click();
+        return;
+      }
+      default:
+        return;
+    }
+    event.preventDefault();
+    this._list.setAttribute("aria-activedescendant", newlyFocusedItem.id);
+    activeDescendant.classList.remove("keyboard-selected");
+    newlyFocusedItem.classList.add("keyboard-selected");
+    newlyFocusedItem.scrollIntoView(false);
+  }
 }
 customElements.define("login-list", LoginList);
--- a/browser/components/aboutlogins/content/components/menu-button.css
+++ b/browser/components/aboutlogins/content/components/menu-button.css
@@ -25,40 +25,39 @@
   margin: 0;
   padding: 5px 0;
   background-color: var(--in-content-box-background);
   border: 1px solid var(--in-content-box-border-color);
   border-radius: 2px;
   box-shadow: var(--shadow-10);
   min-width: max-content;
   list-style-type: none;
+  display: flex;
+  flex-direction: column;
 }
 
 .menu:dir(rtl) {
   right: auto;
   left: 0;
 }
 
-.menuitem {
-  display: flex;
-}
-
 .menuitem-button {
   padding: 4px 8px;
   /* 32px = 8px (padding) + 16px (icon) + 8px (padding) */
   padding-inline-start: 32px;
   background-repeat: no-repeat;
   background-position: left 8px center;
   background-size: 16px;
   margin: 0;
   border-radius: 0;
   /* Override common.inc.css box-shadow on these buttons */
   box-shadow: none !important;
-  flex-grow: 1;
   text-align: start;
+  -moz-context-properties: fill;
+  fill: currentColor;
 }
 
 .menuitem-button:dir(rtl) {
   background-position: right 8px center;
 }
 
 .menuitem-import {
   background-image: url("chrome://browser/skin/save.svg");
@@ -67,11 +66,11 @@
 .menuitem-preferences {
   background-image: url("chrome://browser/skin/settings.svg");
 }
 
 .menuitem-feedback {
   background-image: url("chrome://browser/content/aboutlogins/icons/feedback.svg");
 }
 
-:host(:not(.Win32)) .windows-only {
-  display: none;
+.menuitem-faq {
+  background-image: url("chrome://browser/content/aboutlogins/icons/faq.svg");
 }
--- a/browser/components/aboutlogins/content/components/menu-button.js
+++ b/browser/components/aboutlogins/content/components/menu-button.js
@@ -1,89 +1,106 @@
 /* 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 ReflectedFluentElement from "chrome://browser/content/aboutlogins/components/reflected-fluent-element.js";
-
-export default class MenuButton extends ReflectedFluentElement {
+export default class MenuButton extends HTMLElement {
   connectedCallback() {
     if (this.shadowRoot) {
       return;
     }
 
     let MenuButtonTemplate = document.querySelector("#menu-button-template");
-    this.attachShadow({mode: "open"})
-        .appendChild(MenuButtonTemplate.content.cloneNode(true));
+    let shadowRoot = this.attachShadow({mode: "open"});
+    document.l10n.connectRoot(shadowRoot);
+    shadowRoot.appendChild(MenuButtonTemplate.content.cloneNode(true));
 
-    if (navigator.platform == "Win32") {
-      // We can't add navigator.platform in all cases
-      // because some platforms, such as Ubuntu 64-bit,
-      // use "Linux x86_64" which is an invalid className.
-      this.classList.add(navigator.platform);
+    for (let menuitem of this.shadowRoot.querySelectorAll(".menuitem-button[data-supported-platforms]")) {
+      let supportedPlatforms = menuitem.dataset.supportedPlatforms.split(",").map(platform => platform.trim());
+      if (supportedPlatforms.includes(navigator.platform)) {
+        menuitem.hidden = false;
+      }
     }
 
     this._menu = this.shadowRoot.querySelector(".menu");
     this._menuButton = this.shadowRoot.querySelector(".menu-button");
 
+    this.addEventListener("blur", this);
     this._menuButton.addEventListener("click", this);
-
-    super.connectedCallback();
-  }
-
-  static get reflectedFluentIDs() {
-    return [
-      "button-title",
-      "menuitem-import",
-      "menuitem-feedback",
-      "menuitem-preferences",
-    ];
-  }
-
-  static get observedAttributes() {
-    return MenuButton.reflectedFluentIDs;
-  }
-
-  handleSpecialCaseFluentString(attrName) {
-    if (!this.shadowRoot ||
-        attrName != "button-title") {
-      return false;
-    }
-
-    this._menuButton.setAttribute("title", this.getAttribute(attrName));
-    return true;
+    this.addEventListener("keydown", this, true);
   }
 
   handleEvent(event) {
     switch (event.type) {
+      case "blur": {
+        if (event.relatedTarget &&
+            event.relatedTarget.closest(".menu") == this._menu) {
+          // Only hide the menu if focus has left the menu-button.
+          return;
+        }
+        this._hideMenu();
+        break;
+      }
       case "click": {
         // Skip the catch-all event listener if it was the menu-button
         // that was clicked on.
         if (event.currentTarget == document.documentElement &&
             event.target == this &&
             event.originalTarget == this._menuButton) {
           return;
         }
         let classList = event.originalTarget.classList;
         if (classList.contains("menuitem-import") ||
+            classList.contains("menuitem-faq") ||
             classList.contains("menuitem-feedback") ||
             classList.contains("menuitem-preferences")) {
           let eventName = event.originalTarget.dataset.eventName;
           document.dispatchEvent(new CustomEvent(eventName, {
             bubbles: true,
           }));
           this._hideMenu();
           break;
         }
         this._toggleMenu();
         break;
       }
+      case "keydown": {
+        this._handleKeyDown(event);
+      }
     }
   }
 
+  _handleKeyDown(event) {
+    if (event.key == "Enter") {
+      event.preventDefault();
+      this._toggleMenu();
+    } else if (event.key == "Escape") {
+      this._hideMenu();
+      this._menuButton.focus();
+    }
+
+    if (!event.key.startsWith("Arrow")) {
+      return;
+    }
+
+    let activeMenuitem = this.shadowRoot.activeElement ||
+                         this._menu.querySelector(".menuitem-button:not([hidden])");
+
+    let newlyFocusedItem = null;
+    if (event.key == "ArrowDown") {
+      newlyFocusedItem = activeMenuitem.nextElementSibling;
+    } else if (event.key == "ArrowUp") {
+      newlyFocusedItem = activeMenuitem.previousElementSibling;
+    }
+    if (!newlyFocusedItem) {
+      return;
+    }
+    newlyFocusedItem.focus();
+  }
+
   _hideMenu() {
     this._menu.hidden = true;
     document.documentElement.removeEventListener("click", this, true);
   }
 
   _showMenu() {
     this._menu.hidden = false;
 
deleted file mode 100644
--- a/browser/components/aboutlogins/content/components/reflected-fluent-element.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/* 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/. */
-
-export default class ReflectedFluentElement extends HTMLElement {
-  connectedCallback() {
-    this._reflectFluentStrings();
-  }
-
-  /*
-   * Fluent doesn't handle localizing into Shadow DOM yet so strings
-   * need to get reflected in to their targeted element.
-   */
-  attributeChangedCallback(attr, oldValue, newValue) {
-    if (!this.shadowRoot) {
-      return;
-    }
-
-    // Don't respond to attribute changes that aren't related to locale text.
-    if (!this.constructor.reflectedFluentIDs.includes(attr)) {
-      return;
-    }
-
-    if (this.handleSpecialCaseFluentString &&
-        this.handleSpecialCaseFluentString(attr)) {
-      return;
-    }
-
-    // Strings that are reflected to their shadowed element are assigned
-    // to an attribute name that matches a className on the element.
-    let shadowedElement = this.shadowRoot.querySelector("." + attr);
-    shadowedElement.textContent = newValue;
-  }
-
-  _isReflectedAttributePresent(attr) {
-    return this.constructor.reflectedFluentIDs.includes(attr.name);
-  }
-
-  /*
-   * Called to apply any localized strings that Fluent may have applied
-   * to the element before the custom element was defined.
-   */
-  _reflectFluentStrings() {
-    for (let reflectedFluentID of this.constructor.reflectedFluentIDs) {
-      if (this.hasAttribute(reflectedFluentID)) {
-        if (this.handleSpecialCaseFluentString &&
-            this.handleSpecialCaseFluentString(reflectedFluentID)) {
-          continue;
-        }
-
-        let attrValue = this.getAttribute(reflectedFluentID);
-        // Strings that are reflected to their shadowed element are assigned
-        // to an attribute name that matches a className on the element.
-        let shadowedElement = this.shadowRoot.querySelector("." + reflectedFluentID);
-        shadowedElement.textContent = attrValue;
-      }
-    }
-  }
-}
-customElements.define("reflected-fluent-element", ReflectedFluentElement);
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/content/icons/faq.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+    <path fill="context-fill" fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zM8 2.719a2.925 2.925 0 0 0-3.115 3.114.948.948 0 1 0 1.896 0c0-1.083.65-1.218 1.219-1.218a1.197 1.197 0 0 1 1.224.806c.1.457-.142.92-.574 1.1a2.966 2.966 0 0 0-1.598 2.562v.365a.948.948 0 0 0 1.896 0v-.4c.048-.393.298-.732.66-.893a2.857 2.857 0 0 0 1.447-3.232A3.074 3.074 0 0 0 8 2.72zm0 8.26a1.354 1.354 0 1 0 0 2.708 1.354 1.354 0 0 0 0-2.708z"/>
+</svg>
--- a/browser/components/aboutlogins/jar.mn
+++ b/browser/components/aboutlogins/jar.mn
@@ -1,29 +1,29 @@
 # 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/.
 
 browser.jar:
-  content/browser/aboutlogins/components/copy-to-clipboard-button.css (content/components/copy-to-clipboard-button.css)
-  content/browser/aboutlogins/components/copy-to-clipboard-button.js  (content/components/copy-to-clipboard-button.js)
+  content/browser/aboutlogins/components/confirm-delete-dialog.css      (content/components/confirm-delete-dialog.css)
+  content/browser/aboutlogins/components/confirm-delete-dialog.js       (content/components/confirm-delete-dialog.js)
   content/browser/aboutlogins/components/login-filter.css      (content/components/login-filter.css)
   content/browser/aboutlogins/components/login-filter.js       (content/components/login-filter.js)
   content/browser/aboutlogins/components/login-item.css        (content/components/login-item.css)
   content/browser/aboutlogins/components/login-item.js         (content/components/login-item.js)
   content/browser/aboutlogins/components/login-list.css        (content/components/login-list.css)
   content/browser/aboutlogins/components/login-list.js         (content/components/login-list.js)
   content/browser/aboutlogins/components/login-list-item.css   (content/components/login-list-item.css)
   content/browser/aboutlogins/components/login-list-item.js    (content/components/login-list-item.js)
   content/browser/aboutlogins/components/menu-button.css       (content/components/menu-button.css)
   content/browser/aboutlogins/components/menu-button.js        (content/components/menu-button.js)
-  content/browser/aboutlogins/components/reflected-fluent-element.js  (content/components/reflected-fluent-element.js)
-  content/browser/aboutlogins/icons/delete.svg  (content/icons/delete.svg)
-  content/browser/aboutlogins/icons/edit.svg    (content/icons/edit.svg)
-  content/browser/aboutlogins/icons/favicon.svg   (content/icons/favicon.svg)
-  content/browser/aboutlogins/icons/feedback.svg  (content/icons/feedback.svg)
+  content/browser/aboutlogins/icons/delete.svg        (content/icons/delete.svg)
+  content/browser/aboutlogins/icons/edit.svg          (content/icons/edit.svg)
+  content/browser/aboutlogins/icons/faq.svg           (content/icons/faq.svg)
+  content/browser/aboutlogins/icons/favicon.svg       (content/icons/favicon.svg)
+  content/browser/aboutlogins/icons/feedback.svg      (content/icons/feedback.svg)
   content/browser/aboutlogins/icons/hide-password.svg (content/icons/hide-password.svg)
   content/browser/aboutlogins/icons/show-password.svg (content/icons/show-password.svg)
   content/browser/aboutlogins/aboutLogins.css   (content/aboutLogins.css)
   content/browser/aboutlogins/aboutLogins.js    (content/aboutLogins.js)
   content/browser/aboutlogins/aboutLogins.html  (content/aboutLogins.html)
   content/browser/aboutlogins/aboutLoginsUtils.js (content/aboutLoginsUtils.js)
   content/browser/aboutlogins/common.css        (content/common.css)
--- a/browser/components/aboutlogins/tests/browser/browser.ini
+++ b/browser/components/aboutlogins/tests/browser/browser.ini
@@ -3,16 +3,17 @@ prefs =
   signon.management.page.enabled=true
 support-files =
   head.js
 
 # Run first so content events from previous tests won't trickle in.
 # Skip ASAN and debug since waiting for content events is already slow.
 [browser_aaa_eventTelemetry_run_first.js]
 skip-if = asan || debug
+[browser_confirmDeleteDialog.js]
 [browser_copyToClipboardButton.js]
 [browser_createLogin.js]
 [browser_deleteLogin.js]
 [browser_loginListChanges.js]
 [browser_masterPassword.js]
 [browser_openFiltered.js]
 [browser_openImport.js]
 skip-if = (os != "win") # import is only available on Windows
--- a/browser/components/aboutlogins/tests/browser/browser_aaa_eventTelemetry_run_first.js
+++ b/browser/components/aboutlogins/tests/browser/browser_aaa_eventTelemetry_run_first.js
@@ -38,25 +38,23 @@ add_task(async function test_telemetry_e
     let loginListItem = loginList.shadowRoot.querySelector("login-list-item[data-guid]");
     loginListItem.click();
   });
   await waitForTelemetryEventCount(1);
 
   await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
     let loginItem = content.document.querySelector("login-item");
     let copyButton = loginItem.shadowRoot.querySelector(".copy-username-button");
-    copyButton = copyButton.shadowRoot.querySelector(".copy-button");
     copyButton.click();
   });
   await waitForTelemetryEventCount(2);
 
   await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
     let loginItem = content.document.querySelector("login-item");
     let copyButton = loginItem.shadowRoot.querySelector(".copy-password-button");
-    copyButton = copyButton.shadowRoot.querySelector(".copy-button");
     copyButton.click();
   });
   await waitForTelemetryEventCount(3);
 
   let promiseNewTab = BrowserTestUtils.waitForNewTab(gBrowser, TEST_LOGIN1.origin);
   await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
     let loginItem = content.document.querySelector("login-item");
     let openSiteButton = loginItem.shadowRoot.querySelector(".open-site-button");
@@ -103,16 +101,19 @@ add_task(async function test_telemetry_e
     loginListItem.click();
   });
   await waitForTelemetryEventCount(9);
 
   await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
     let loginItem = content.document.querySelector("login-item");
     let deleteButton = loginItem.shadowRoot.querySelector(".delete-button");
     deleteButton.click();
+    let confirmDeleteDialog = content.document.querySelector("confirm-delete-dialog");
+    let confirmDeleteButton = confirmDeleteDialog.shadowRoot.querySelector(".confirm-button");
+    confirmDeleteButton.click();
   });
   await waitForTelemetryEventCount(10);
 
   await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
     let loginFilter = content.document.querySelector("login-filter");
     let input = loginFilter.shadowRoot.querySelector("input");
     input.setUserInput("test");
   });
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/tests/browser/browser_confirmDeleteDialog.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function setup() {
+  await BrowserTestUtils.openNewForegroundTab({gBrowser, url: "about:logins"});
+  registerCleanupFunction(() => {
+    BrowserTestUtils.removeTab(gBrowser.selectedTab);
+  });
+});
+
+add_task(async function test() {
+  let browser = gBrowser.selectedBrowser;
+
+  await ContentTask.spawn(browser, null, async () => {
+    let dialog = Cu.waiveXrays(content.document.querySelector("confirm-delete-dialog"));
+
+    let cancelButton = dialog.shadowRoot.querySelector(".cancel-button");
+    let confirmDeleteButton = dialog.shadowRoot.querySelector(".confirm-button");
+    let dismissButton = dialog.shadowRoot.querySelector(".dismiss-button");
+    let message = dialog.shadowRoot.querySelector(".message");
+    let title = dialog.shadowRoot.querySelector(".title");
+
+    is(title.textContent, "Confirm Deletion",
+       "Title contents should match l10n attribute set on outer element");
+    is(message.textContent, "Are you sure you want to delete this login?",
+       "Message contents should match l10n attribute set on outer element");
+    is(cancelButton.textContent, "Cancel",
+       "Cancel button contents should match l10n attribute set on outer element");
+    is(confirmDeleteButton.textContent, "Delete login",
+       "Delete button contents should match l10n attribute set on outer element");
+
+    let showPromise = dialog.show();
+    cancelButton.click();
+    try {
+      await showPromise;
+      ok(false, "Promise returned by show() should not resolve after clicking cancel button");
+    } catch (ex) {
+      ok(true, "Promise returned by show() should reject after clicking cancel button");
+    }
+    await ContentTaskUtils.waitForCondition(() => dialog.hidden,
+      "Waiting for the dialog to be hidden");
+    ok(dialog.hidden, "Dialog should be hidden after clicking cancel button");
+
+    showPromise = dialog.show();
+    dismissButton.click();
+    try {
+      await showPromise;
+      ok(false, "Promise returned by show() should not resolve after clicking dismiss button");
+    } catch (ex) {
+      ok(true, "Promise returned by show() should reject after clicking dismiss button");
+    }
+    await ContentTaskUtils.waitForCondition(() => dialog.hidden,
+      "Waiting for the dialog to be hidden");
+    ok(dialog.hidden, "Dialog should be hidden after clicking dismiss button");
+
+    showPromise = dialog.show();
+    confirmDeleteButton.click();
+    try {
+      await showPromise;
+      ok(true, "Promise returned by show() should resolve after clicking confirm button");
+    } catch (ex) {
+      ok(false, "Promise returned by show() should not reject after clicking confirm button");
+    }
+    await ContentTaskUtils.waitForCondition(() => dialog.hidden,
+      "Waiting for the dialog to be hidden");
+    ok(dialog.hidden, "Dialog should be hidden after clicking confirm button");
+  });
+});
--- a/browser/components/aboutlogins/tests/browser/browser_copyToClipboardButton.js
+++ b/browser/components/aboutlogins/tests/browser/browser_copyToClipboardButton.js
@@ -14,18 +14,17 @@ add_task(async function test() {
 
     await ContentTask.spawn(browser, TEST_LOGIN, async function(login) {
       let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
 
       // The login object needs to be cloned into the content global.
       loginItem.setLogin(Cu.cloneInto(login, content));
 
       // Lower the timeout for the test.
-      let copyButton = loginItem.shadowRoot.querySelector(".copy-username-button");
-      Object.defineProperty(copyButton.constructor, "BUTTON_RESET_TIMEOUT", {
+      Object.defineProperty(loginItem.constructor, "COPY_BUTTON_RESET_TIMEOUT", {
           configurable: true,
           writable: true,
           value: 1000,
       });
     });
 
     for (let testCase of [
       [TEST_LOGIN.username, ".copy-username-button"],
@@ -35,19 +34,18 @@ add_task(async function test() {
         expectedValue: testCase[0],
         copyButtonSelector: testCase[1],
       };
       info("waiting for " + testObj.expectedValue + " to be placed on clipboard");
       await SimpleTest.promiseClipboardChange(testObj.expectedValue, async () => {
         await ContentTask.spawn(browser, testObj, async function(aTestObj) {
           let loginItem = content.document.querySelector("login-item");
           let copyButton = loginItem.shadowRoot.querySelector(aTestObj.copyButtonSelector);
-          let innerButton = copyButton.shadowRoot.querySelector("button");
           info("Clicking 'copy' button");
-          innerButton.click();
+          copyButton.click();
         });
       });
       ok(true, testObj.expectedValue + " is on clipboard now");
 
       await ContentTask.spawn(browser, testObj, async function(aTestObj) {
         let loginItem = content.document.querySelector("login-item");
         let copyButton = loginItem.shadowRoot.querySelector(aTestObj.copyButtonSelector);
         ok(copyButton.dataset.copied, "Success message should be shown");
--- a/browser/components/aboutlogins/tests/browser/browser_deleteLogin.js
+++ b/browser/components/aboutlogins/tests/browser/browser_deleteLogin.js
@@ -40,12 +40,16 @@ add_task(async function test_login_item(
     let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
     let loginItemPopulated = await ContentTaskUtils.waitForCondition(() => {
       return loginItem._login.guid == loginListItem.dataset.guid;
     }, "Waiting for login item to get populated");
     ok(loginItemPopulated, "The login item should get populated");
 
     let deleteButton = loginItem.shadowRoot.querySelector(".delete-button");
     deleteButton.click();
+
+    let confirmDeleteDialog = Cu.waiveXrays(content.document.querySelector("confirm-delete-dialog"));
+    let confirmButton = confirmDeleteDialog.shadowRoot.querySelector(".confirm-button");
+    confirmButton.click();
   });
   ok(deleteLoginMessageReceived,
      "Clicking the delete button should send the AboutLogins:DeleteLogin messsage");
 });
--- a/browser/components/aboutlogins/tests/browser/browser_updateLogin.js
+++ b/browser/components/aboutlogins/tests/browser/browser_updateLogin.js
@@ -75,16 +75,19 @@ add_task(async function test_login_item(
     ok(!loginItem.dataset.editing, "LoginItem should not be in 'edit' mode after saving");
 
     editButton.click();
     await Promise.resolve();
 
     ok(loginItem.dataset.editing, "LoginItem should be in 'edit' mode");
     let deleteButton = loginItem.shadowRoot.querySelector(".delete-button");
     deleteButton.click();
+    let confirmDeleteDialog = Cu.waiveXrays(content.document.querySelector("confirm-delete-dialog"));
+    let confirmDeleteButton = confirmDeleteDialog.shadowRoot.querySelector(".confirm-button");
+    confirmDeleteButton.click();
 
     await ContentTaskUtils.waitForCondition(() => {
       loginListItem = Cu.waiveXrays(loginList.shadowRoot.querySelector("login-list-item"));
       return !loginListItem;
     }, "Waiting for login to be removed from list");
 
     ok(!loginItem.dataset.editing, "LoginItem should not be in 'edit' mode after deleting");
   });
--- a/browser/components/aboutlogins/tests/chrome/aboutlogins_common.js
+++ b/browser/components/aboutlogins/tests/chrome/aboutlogins_common.js
@@ -1,11 +1,11 @@
 "use strict";
 
-/* exported asyncElementRendered, importDependencies, stubFluentL10n */
+/* exported asyncElementRendered, importDependencies */
 
 /**
  * A helper to await on while waiting for an asynchronous rendering of a Custom
  * Element.
  * @returns {Promise}
  */
 function asyncElementRendered() {
   return Promise.resolve();
@@ -20,23 +20,29 @@ function importDependencies(templateFram
   let templates = templateFrame.contentDocument.querySelectorAll("template");
   isnot(templates, null, "Check some templates found");
   for (let template of templates) {
     let imported = document.importNode(template, true);
     destinationEl.appendChild(imported);
   }
 }
 
-function stubFluentL10n(argsMap) {
-  Object.defineProperty(document, "l10n", {
-    configurable: true,
-    writable: true,
-    value: {
-      setAttributes(element, id, args) {
-        element.setAttribute("data-l10n-id", id);
-        for (let attrName of Object.keys(argsMap)) {
-          let varName = argsMap[attrName];
-          element.setAttribute(attrName, args[varName]);
-        }
-      },
+Object.defineProperty(document, "l10n", {
+  configurable: true,
+  writable: true,
+  value: {
+    connectRoot() {
+    },
+    translateElements() {
+      return Promise.resolve();
     },
-  });
-}
+    getAttributes(element) {
+      return {
+        id: element.getAttribute("data-l10n-id"),
+        args: JSON.parse(element.getAttribute("data-l10n-args")),
+      };
+    },
+    setAttributes(element, id, args) {
+      element.setAttribute("data-l10n-id", id);
+      element.setAttribute("data-l10n-args", JSON.stringify(args));
+    },
+  },
+});
--- a/browser/components/aboutlogins/tests/chrome/chrome.ini
+++ b/browser/components/aboutlogins/tests/chrome/chrome.ini
@@ -1,10 +1,10 @@
 [DEFAULT]
 scheme = https
 support-files =
    aboutlogins_common.js
 
+[test_confirm_delete_dialog.html]
 [test_login_filter.html]
 [test_login_item.html]
 [test_login_list.html]
 [test_menu_button.html]
-[test_reflected_fluent_element.html]
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutlogins/tests/chrome/test_confirm_delete_dialog.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the confirm-delete-dialog component
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the confirm-delete-dialog component</title>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+  <script type="module" src="chrome://browser/content/aboutlogins/components/confirm-delete-dialog.js"></script>
+  <script src="aboutlogins_common.js"></script>
+
+  <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <p id="display">
+  </p>
+<div id="content" style="display: none">
+  <iframe id="templateFrame" src="chrome://browser/content/aboutlogins/aboutLogins.html"
+          sandbox="allow-same-origin"></iframe>
+</div>
+<pre id="test">
+</pre>
+<script>
+/** Test the confirm-delete-dialog component **/
+
+let cancelButton, confirmButton, gConfirmDeleteDialog;
+add_task(async function setup() {
+  let templateFrame = document.getElementById("templateFrame");
+  let displayEl = document.getElementById("display");
+  importDependencies(templateFrame, displayEl);
+
+  gConfirmDeleteDialog = document.createElement("confirm-delete-dialog");
+  displayEl.appendChild(gConfirmDeleteDialog);
+  ok(gConfirmDeleteDialog, "The dialog should exist");
+
+  cancelButton = gConfirmDeleteDialog.shadowRoot.querySelector(".cancel-button");
+  confirmButton = gConfirmDeleteDialog.shadowRoot.querySelector(".confirm-button");
+  ok(cancelButton, "The cancel button should exist");
+  ok(confirmButton, "The confirm button should exist");
+});
+
+add_task(async function test_escape_key_to_cancel() {
+  gConfirmDeleteDialog.show();
+  ok(!gConfirmDeleteDialog.hidden, "The dialog should be visible");
+  sendKey("ESCAPE");
+  ok(gConfirmDeleteDialog.hidden, "The dialog should be hidden after hitting Escape");
+  gConfirmDeleteDialog.hide();
+});
+
+add_task(async function test_initial_focus() {
+  gConfirmDeleteDialog.show();
+  ok(!gConfirmDeleteDialog.hidden, "The dialog should be visible");
+  is(gConfirmDeleteDialog.shadowRoot.activeElement, cancelButton,
+     "After initially opening the dialog, the cancel button should be focused");
+  gConfirmDeleteDialog.hide();
+});
+
+add_task(async function test_tab_focus() {
+  gConfirmDeleteDialog.show();
+  ok(!gConfirmDeleteDialog.hidden, "The dialog should be visible");
+  sendKey("TAB");
+  is(gConfirmDeleteDialog.shadowRoot.activeElement, confirmButton,
+     "After opening the dialog and tabbing once, the confirm delete button should be focused");
+  gConfirmDeleteDialog.hide();
+});
+
+add_task(async function test_enter_key_to_cancel() {
+  let showPromise = gConfirmDeleteDialog.show();
+  ok(!gConfirmDeleteDialog.hidden, "The dialog should be visible");
+  sendKey("RETURN");
+  try {
+    await showPromise;
+    ok(false, "The dialog Promise should not resolve after hitting Return with the cancel button focused");
+  } catch (ex) {
+    ok(true, "The dialog Promise should reject after hitting Return with the cancel button focused");
+  }
+});
+
+add_task(async function test_enter_key_to_confirm() {
+  let showPromise = gConfirmDeleteDialog.show();
+  ok(!gConfirmDeleteDialog.hidden, "The dialog should be visible");
+  sendKey("TAB");
+  sendKey("RETURN");
+  try {
+    await showPromise;
+    ok(true, "The dialog Promise should resolve after hitting Return with the confirm button focused");
+  } catch (ex) {
+    ok(false, "The dialog Promise should not reject after hitting Return with the confirm button focused");
+  }
+});
+</script>
+</body>
+</html>
--- a/browser/components/aboutlogins/tests/chrome/test_login_filter.html
+++ b/browser/components/aboutlogins/tests/chrome/test_login_filter.html
@@ -24,20 +24,16 @@ Test the login-filter component
 <pre id="test">
 </pre>
 <script>
 /** Test the login-filter component **/
 
 let gLoginFilter;
 let gLoginList;
 add_task(async function setup() {
-  stubFluentL10n({
-    "count": "count",
-  });
-
   let templateFrame = document.getElementById("templateFrame");
   let displayEl = document.getElementById("display");
   importDependencies(templateFrame, displayEl);
 
   gLoginFilter = document.createElement("login-filter");
   displayEl.appendChild(gLoginFilter);
 
   gLoginList = document.createElement("login-list");
@@ -108,20 +104,18 @@ add_task(async function test_list_filter
 
     let filterLength = loginFilterInput.value.length;
     while (filterLength-- > 0) {
       sendKey("BACK_SPACE");
     }
     sendString(testObj.query);
 
     await SimpleTest.promiseWaitForCondition(() => {
-      return gLoginList.hasAttribute("count") &&
-             +gLoginList.getAttribute("count") == testObj.resultExpectedCount;
+      let countElement = gLoginList.shadowRoot.querySelector(".count");
+      return countElement.hasAttribute("data-l10n-args") &&
+             JSON.parse(countElement.getAttribute("data-l10n-args")).count == testObj.resultExpectedCount;
     }, `Waiting for the search result count to update to ${testObj.resultExpectedCount} (tc#${testObj.testCase})`);
-    let count = +gLoginList.getAttribute("count");
-    is(count, testObj.resultExpectedCount,
-       `The login list count should match the expected result (tc#${testObj.testCase})`);
   }
 });
 </script>
 
 </body>
 </html>
--- a/browser/components/aboutlogins/tests/chrome/test_login_item.html
+++ b/browser/components/aboutlogins/tests/chrome/test_login_item.html
@@ -42,70 +42,64 @@ const TEST_LOGIN_2 = {
   username: "user2",
   password: "pass2",
   timeCreated: "2000",
   timePasswordChanged: "4000",
   timeLastUsed: "8000",
 };
 
 add_task(async function setup() {
-  stubFluentL10n({
-    "time-created": "timeCreated",
-    "time-changed": "timeChanged",
-    "time-used": "timeUsed",
-  });
-
   let templateFrame = document.getElementById("templateFrame");
   let displayEl = document.getElementById("display");
   importDependencies(templateFrame, displayEl);
 
   gLoginItem = document.createElement("login-item");
   displayEl.appendChild(gLoginItem);
 });
 
 add_task(async function test_empty_item() {
   ok(gLoginItem, "loginItem exists");
   is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, "", "origin should be blank");
   is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be blank");
   is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, "", "password should be blank");
-  is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, "", "time-created should be blank when undefined");
-  is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be blank when undefined");
-  is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be blank when undefined");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, "", "time-created should be blank when undefined");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, "", "time-changed should be blank when undefined");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, "", "time-used should be blank when undefined");
 });
 
 add_task(async function test_set_login() {
   gLoginItem.setLogin(TEST_LOGIN_1);
   await asyncElementRendered();
 
   ok(!gLoginItem.dataset.editing, "loginItem should not be in 'edit' mode");
   ok(!gLoginItem.dataset.isNewLogin, "loginItem should not be in 'isNewLogin' mode");
   is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, TEST_LOGIN_1.origin, "origin should be populated");
   is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be populated");
   is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be populated");
-  is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be populated");
-  is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be populated");
-  is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be populated");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, TEST_LOGIN_1.timeCreated, "time-created should be populated");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, TEST_LOGIN_1.timePasswordChanged, "time-changed should be populated");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, TEST_LOGIN_1.timeLastUsed, "time-used should be populated");
 });
 
 add_task(async function test_edit_login() {
   gLoginItem.setLogin(TEST_LOGIN_1);
   gLoginItem.shadowRoot.querySelector(".edit-button").click();
   await asyncElementRendered();
 
   ok(gLoginItem.dataset.editing, "loginItem should be in 'edit' mode");
   ok(isHidden(gLoginItem.shadowRoot.querySelector(".edit-button")), "edit button should be hidden in 'edit' mode");
   ok(!gLoginItem.dataset.isNewLogin, "loginItem should not be in 'isNewLogin' mode");
   let deleteButton = gLoginItem.shadowRoot.querySelector(".delete-button");
   ok(!deleteButton.disabled, "Delete button should be enabled when editing a login");
   is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, TEST_LOGIN_1.origin, "origin should be populated");
   is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be populated");
   is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be populated");
-  is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be populated");
-  is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be populated");
-  is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be populated");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, TEST_LOGIN_1.timeCreated, "time-created should be populated");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, TEST_LOGIN_1.timePasswordChanged, "time-changed should be populated");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, TEST_LOGIN_1.timeLastUsed, "time-used should be populated");
 
   gLoginItem.shadowRoot.querySelector("input[name='username']").value = "newUsername";
   gLoginItem.shadowRoot.querySelector("input[name='password']").value = "newPassword";
 
   let updateEventDispatched = false;
   document.addEventListener("AboutLoginsUpdateLogin", event => {
     is(event.detail.guid, TEST_LOGIN_1.guid, "event should include guid");
     is(event.detail.origin, TEST_LOGIN_1.origin, "event should include origin");
@@ -153,19 +147,19 @@ add_task(async function test_set_login_e
   ok(gLoginItem.dataset.editing, "loginItem should be in 'edit' mode");
   ok(isHidden(gLoginItem.shadowRoot.querySelector(".edit-button")), "edit button should be hidden in 'edit' mode");
   ok(gLoginItem.dataset.isNewLogin, "loginItem should be in 'isNewLogin' mode");
   let deleteButton = gLoginItem.shadowRoot.querySelector(".delete-button");
   ok(deleteButton.disabled, "Delete button should be disabled when creating a login");
   is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, "", "origin should be empty");
   is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be empty");
   is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, "", "password should be empty");
-  is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, "", "time-created should be blank when undefined");
-  is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be blank when undefined");
-  is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be blank when undefined");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, "", "time-created should be blank when undefined");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, "", "time-changed should be blank when undefined");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, "", "time-used should be blank when undefined");
 
   let createEventDispatched = false;
   document.addEventListener("AboutLoginsCreateLogin", event => {
     createEventDispatched = true;
   }, {once: true});
   gLoginItem.shadowRoot.querySelector(".save-changes-button").click();
   ok(!createEventDispatched, "Clicking the .save-changes-button shouldn't dispatch the event when fields are invalid");
   let originInput = gLoginItem.shadowRoot.querySelector("input[name='origin']");
@@ -206,58 +200,58 @@ add_task(async function test_different_l
   gLoginItem.setLogin(TEST_LOGIN_1);
   let otherLogin = Object.assign({}, TEST_LOGIN_1, {username: "fakeuser", guid: "fakeguid"});
   gLoginItem.loginModified(otherLogin);
   await asyncElementRendered();
 
   is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, TEST_LOGIN_1.origin, "origin should be unchanged");
   is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
   is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
-  is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
-  is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
-  is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
 });
 
 add_task(async function test_different_login_removed() {
   gLoginItem.setLogin(TEST_LOGIN_1);
   let otherLogin = Object.assign({}, TEST_LOGIN_1, {username: "fakeuser", guid: "fakeguid"});
   gLoginItem.loginRemoved(otherLogin);
   await asyncElementRendered();
 
   is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, TEST_LOGIN_1.origin, "origin should be unchanged");
   is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
   is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
-  is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
-  is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
-  is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
 });
 
 add_task(async function test_login_modified() {
   gLoginItem.setLogin(TEST_LOGIN_1);
   let modifiedLogin = Object.assign({}, TEST_LOGIN_1, {username: "updateduser"});
   gLoginItem.loginModified(modifiedLogin);
   await asyncElementRendered();
 
   is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, modifiedLogin.origin, "origin should be updated");
   is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, modifiedLogin.username, "username should be updated");
   is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, modifiedLogin.password, "password should be updated");
-  is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, modifiedLogin.timeCreated, "time-created should be updated");
-  is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, modifiedLogin.timePasswordChanged, "time-changed should be updated");
-  is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, modifiedLogin.timeLastUsed, "time-used should be updated");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, modifiedLogin.timeCreated, "time-created should be updated");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, modifiedLogin.timePasswordChanged, "time-changed should be updated");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, modifiedLogin.timeLastUsed, "time-used should be updated");
 });
 
 add_task(async function test_login_removed() {
   gLoginItem.setLogin(TEST_LOGIN_1);
   gLoginItem.loginRemoved(TEST_LOGIN_1);
   await asyncElementRendered();
 
   is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, "", "origin should be cleared");
   is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be cleared");
   is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, "", "password should be cleared");
-  is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, "", "time-created should be blank when undefined");
-  is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be blank when undefined");
-  is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be blank when undefined");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, "", "time-created should be blank when undefined");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, "", "time-changed should be blank when undefined");
+  is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, "", "time-used should be blank when undefined");
 });
 
 </script>
 
 </body>
 </html>
--- a/browser/components/aboutlogins/tests/chrome/test_login_list.html
+++ b/browser/components/aboutlogins/tests/chrome/test_login_list.html
@@ -2,16 +2,17 @@
 <html>
 <!--
 Test the login-list component
 -->
 <head>
   <meta charset="utf-8">
   <title>Test the login-list component</title>
   <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
   <script type="module" src="chrome://browser/content/aboutlogins/components/login-list.js"></script>
   <script src="aboutlogins_common.js"></script>
 
   <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
   <p id="display">
   </p>
@@ -52,44 +53,91 @@ const TEST_LOGIN_3 = {
   password: "pass3",
   title: "def.example.com",
   // new Date("June 1, 2019").getTime()
   timeLastUsed: 1559361600000,
   timePasswordChanged: 1559361600000,
 };
 
 add_task(async function setup() {
-  stubFluentL10n({
-    "count": "count",
-  });
-
   let templateFrame = document.getElementById("templateFrame");
   let displayEl = document.getElementById("display");
   importDependencies(templateFrame, displayEl);
 
   gLoginList = document.createElement("login-list");
   displayEl.appendChild(gLoginList);
 });
 
 add_task(async function test_empty_list() {
   ok(gLoginList, "loginList exists");
   is(gLoginList.textContent, "", "Initially empty");
 });
 
+add_task(async function test_keyboard_navigation() {
+  gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2, TEST_LOGIN_3]);
+
+  while (document.activeElement != gLoginList) {
+    sendKey("TAB");
+    await new Promise(resolve => requestAnimationFrame(resolve));
+  }
+
+  sendKey("TAB");
+  sendKey("TAB");
+  let loginSort = gLoginList.shadowRoot.querySelector("#login-sort");
+  await SimpleTest.promiseWaitForCondition(() => loginSort == gLoginList.shadowRoot.activeElement,
+    "waiting for login-sort to get focus");
+  ok(loginSort == gLoginList.shadowRoot.activeElement, "#login-sort should be focused after tabbing to it");
+
+  sendKey("TAB");
+  let ol = gLoginList.shadowRoot.querySelector("ol");
+  await SimpleTest.promiseWaitForCondition(() => ol.matches(":focus"),
+    "waiting for 'ol' to get focus");
+  ok(ol.matches(":focus"), "'ol' should be focused after tabbing to it");
+
+  for (let [keyFwd, keyRev] of [["LEFT", "RIGHT"], ["DOWN", "UP"]]) {
+    sendKey(keyFwd);
+    await SimpleTest.promiseWaitForCondition(() => ol.getAttribute("aria-activedescendant") == ol.children[1].id,
+      `waiting for second item in list to get focused (${keyFwd})`);
+    ok(ol.children[1].classList.contains("keyboard-selected"), `second item should be marked as keyboard-selected (${keyFwd})`);
+
+    sendKey(keyRev);
+    await SimpleTest.promiseWaitForCondition(() => ol.getAttribute("aria-activedescendant") == ol.children[0].id,
+      `waiting for first item in list to get focused (${keyRev})`);
+    ok(ol.children[0].classList.contains("keyboard-selected"), `first item should be marked as keyboard-selected (${keyRev})`);
+  }
+
+  sendKey("DOWN");
+  await SimpleTest.promiseWaitForCondition(() => ol.getAttribute("aria-activedescendant") == ol.children[1].id,
+    `waiting for second item in list to get focused (DOWN)`);
+  ok(ol.children[1].classList.contains("keyboard-selected"), `second item should be marked as keyboard-selected (DOWN)`);
+  let selectedGuid = ol.children[1].dataset.guid;
+
+  let loginSelectedEvent = null;
+  gLoginList.addEventListener("AboutLoginsLoginSelected", event => loginSelectedEvent = event, {once: true});
+  sendKey("RETURN");
+  is(ol.querySelector(".selected").dataset.guid, selectedGuid, "item should be marked as selected");
+  ok(loginSelectedEvent, "AboutLoginsLoginSelected event should be dispatched on pressing Enter");
+  is(loginSelectedEvent.detail.guid, selectedGuid, "event should have expected login attached");
+});
+
 add_task(async function test_empty_login_username_in_list() {
+  // Clear the selection so the 'new' login will be in the list too.
+  window.dispatchEvent(new CustomEvent("AboutLoginsLoginSelected", {
+    detail: {},
+  }));
+
   gLoginList.setLogins([TEST_LOGIN_3]);
   let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
   is(loginListItems.length, 2, "A blank login and the one stored login should be displayed");
   ok(!loginListItems[0].dataset.guid, "first login-list-item should be the 'new' item");
   is(loginListItems[1].dataset.guid, TEST_LOGIN_3.guid, "login-list-item should have correct guid attribute");
 
-  loginListItems[1].setAttribute("missing-username", "(no username)");
   loginListItems[1].render();
   let loginUsername = loginListItems[1].shadowRoot.querySelector(".username");
-  is(loginUsername.textContent, "(no username)", "login should show missing username text");
+  is(loginUsername.getAttribute("data-l10n-id"), "login-list-item-subtitle-missing-username", "login should show missing username text");
 });
 
 add_task(async function test_populated_list() {
   gLoginList.setLogins([TEST_LOGIN_1, TEST_LOGIN_2]);
   let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
   is(loginListItems.length, 3, "A blank login and the two stored logins should be displayed");
   ok(!loginListItems[0].dataset.guid, "first login-list-item should be the 'new' item");
   is(loginListItems[1].dataset.guid, TEST_LOGIN_1.guid, "login-list-item should have correct guid attribute");
@@ -105,55 +153,55 @@ add_task(async function test_populated_l
   is(loginListItems.length, 2, "After selecting one, only the two stored logins should be displayed");
   ok(loginListItems[0].classList.contains("selected"), "The first item should be selected");
   ok(!loginListItems[1].classList.contains("selected"), "The second item should still not be selected");
 });
 
 add_task(async function test_filtered_list() {
   is(gLoginList.shadowRoot.querySelectorAll("login-list-item:not([hidden])").length, 2, "Both logins should be visible");
   let countSpan = gLoginList.shadowRoot.querySelector(".count");
-  is(countSpan.textContent, "2", "Count should match full list length");
+  is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should match full list length");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     detail: "user1",
   }));
-  is(countSpan.textContent, "1", "Count should match result amount");
+  is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should match result amount");
   let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
   is(loginListItems[0].shadowRoot.querySelector(".username").textContent, "user1", "user1 is expected first");
   ok(!loginListItems[0].hidden, "user1 should remain visible");
   ok(loginListItems[1].hidden, "user2 should be hidden");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     detail: "user2",
   }));
-  is(countSpan.textContent, "1", "Count should match result amount");
+  is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should match result amount");
   loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
   ok(loginListItems[0].hidden, "user1 should be hidden");
   ok(!loginListItems[1].hidden, "user2 should be visible");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     detail: "user",
   }));
-  is(countSpan.textContent, "2", "Count should match result amount");
+  is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should match result amount");
   loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
   ok(!loginListItems[0].hidden, "user1 should be visible");
   ok(!loginListItems[1].hidden, "user2 should be visible");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     detail: "foo",
   }));
-  is(countSpan.textContent, "0", "Count should match result amount");
+  is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 0, "Count should match result amount");
   loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
   ok(loginListItems[0].hidden, "user1 should be hidden");
   ok(loginListItems[1].hidden, "user2 should be hidden");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     detail: "",
   }));
-  is(countSpan.textContent, "2", "Count should be reset to full list length");
+  is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should be reset to full list length");
   loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
   ok(!loginListItems[0].hidden, "user1 should be visible");
   ok(!loginListItems[1].hidden, "user2 should be visible");
 });
 
 add_task(async function test_login_modified() {
   let modifiedLogin = Object.assign(TEST_LOGIN_1, {username: "user11"});
   gLoginList.loginModified(modifiedLogin);
@@ -190,36 +238,36 @@ add_task(async function test_login_remov
   let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
   is(loginListItems.length, 2, "New login should be removed from the list");
   is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item1 should have correct guid attribute");
   is(loginListItems[1].dataset.guid, TEST_LOGIN_2.guid, "login-list-item2 should have correct guid attribute");
 });
 
 add_task(async function test_login_added_filtered() {
   let countSpan = gLoginList.shadowRoot.querySelector(".count");
-  is(countSpan.textContent, "2", "Count should match full list length");
+  is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should match full list length");
   window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
     bubbles: true,
     composed: true,
     detail: "user1",
   }));
-  is(countSpan.textContent, "1", "Count should match result amount");
+  is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should match result amount");
 
   let newLogin = Object.assign({}, TEST_LOGIN_1, {username: "user22", guid: "111222"});
   gLoginList.loginAdded(newLogin);
   await asyncElementRendered();
   let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
   is(loginListItems.length, 3, "New login should be added to the list");
   is(loginListItems[0].dataset.guid, TEST_LOGIN_1.guid, "login-list-item1 should have correct guid attribute");
   is(loginListItems[1].dataset.guid, TEST_LOGIN_2.guid, "login-list-item2 should have correct guid attribute");
   is(loginListItems[2].dataset.guid, newLogin.guid, "login-list-item3 should have correct guid attribute");
   ok(!loginListItems[0].hidden, "login-list-item1 should be visible");
   ok(loginListItems[1].hidden, "login-list-item2 should be hidden");
   ok(loginListItems[2].hidden, "login-list-item3 should be hidden");
-  is(countSpan.textContent, "1", "Count should remain unchanged");
+  is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should remain unchanged");
 });
 
 add_task(async function test_sorted_list() {
   // Clear the selection so the 'new' login will be in the list too.
   window.dispatchEvent(new CustomEvent("AboutLoginsLoginSelected", {
     detail: {},
   }));
 
--- a/browser/components/aboutlogins/tests/chrome/test_menu_button.html
+++ b/browser/components/aboutlogins/tests/chrome/test_menu_button.html
@@ -43,43 +43,65 @@ add_task(async function setup() {
 
 add_task(async function test_menu_open_close() {
   is(document.activeElement, gMenuButton, "menu-button should be focused to start the test");
 
   let menu = gMenuButton.shadowRoot.querySelector(".menu");
   is(true, menu.hidden, "menu should be hidden before pressing 'space'");
   sendKey("SPACE");
   await new Promise(resolve => requestAnimationFrame(resolve));
-  is(false, menu.hidden, "menu should be visible after pressing 'space'");
+  ok(!menu.hidden, "menu should be visible after pressing 'space'");
+
+  sendKey("ESCAPE");
+  await new Promise(resolve => requestAnimationFrame(resolve));
+  ok(menu.hidden, "menu should be hidden after pressing 'escape'");
+  is(gMenuButton.shadowRoot.activeElement, gMenuButton.shadowRoot.querySelector(".menu-button"),
+    "the .menu-button should be focused after closing the menu via keyboard");
+
+  sendKey("RETURN");
+  await new Promise(resolve => requestAnimationFrame(resolve));
+  ok(!menu.hidden, "menu should be visible after pressing 'return'");
 
-  let feedbackItem = gMenuButton.shadowRoot.querySelector(".menuitem-feedback");
-  ok(!feedbackItem.matches(":focus"), ".menuitem-feedback should not be focused before tabbing to it");
-  // The Import menuitem is only visible on Windows, where we will need a second Tab
-  // press to get to the Feedback item.
-  let tabs = navigator.platform == "Win32" ? 2 : 1;
-  while (tabs--) {
+  let firstVisibleItem = gMenuButton.shadowRoot.querySelector(".menuitem-button:not([hidden])");
+  ok(!firstVisibleItem.matches(":focus"), "the first item should not be focused before tabbing to it");
+  sendKey("TAB");
+  await SimpleTest.promiseWaitForCondition(() => firstVisibleItem.matches(":focus"),
+    "waiting for firstVisibleItem to get focus");
+  ok(firstVisibleItem.matches(":focus"), "firstVisibleItem should be focused after tabbing to it");
+  synthesizeKey("VK_TAB", { shiftKey: true });
+  await SimpleTest.promiseWaitForCondition(() => !firstVisibleItem.matches(":focus"),
+    "waiting for firstVisibleItem to lose focus");
+  ok(!firstVisibleItem.matches(":focus"), "firstVisibleItem should lose focus after tabbing away from it");
+  sendKey("TAB");
+  await SimpleTest.promiseWaitForCondition(() => firstVisibleItem.matches(":focus"),
+    "waiting for firstVisibleItem to get focus again");
+  ok(firstVisibleItem.matches(":focus"), "firstVisibleItem should be focused after tabbing to it again");
+
+  if (navigator.platform == "Win32") {
+    // The Import menuitem is only visible on Windows, where we will need another Tab
+    // press to get to the Preferences item.
+    let preferencesItem = gMenuButton.shadowRoot.querySelector(".menuitem-preferences");
+    sendKey("DOWN");
+    await SimpleTest.promiseWaitForCondition(() => preferencesItem.matches(":focus"),
+      "waiting for preferencesItem to gain focus");
+    ok(preferencesItem.matches(":focus"), `.menuitem-preferences should be now be focused (DOWN)`);
+    sendKey("UP");
+    await SimpleTest.promiseWaitForCondition(() => !preferencesItem.matches(":focus"),
+      `waiting for preferencesItem to lose focus (UP)`);
+    ok(!preferencesItem.matches(":focus"), `.menuitem-preferences should lose focus after pressing up`);
+
     sendKey("TAB");
+    await SimpleTest.promiseWaitForCondition(() => preferencesItem.matches(":focus"),
+      "waiting for preferencesItem to get focus");
+    ok(preferencesItem.matches(":focus"), ".menuitem-preferences should be focused after tabbing to it");
   }
 
-  await SimpleTest.promiseWaitForCondition(() => feedbackItem.matches(":focus"),
-                                           "waiting for feedbackItem to get focus");
-  ok(feedbackItem.matches(":focus"), ".menuitem-feedback should be focused after tabbing to it");
-
-  let preferencesItem = gMenuButton.shadowRoot.querySelector(".menuitem-preferences");
-  ok(!preferencesItem.matches(":focus"), ".menuitem-preferences should not be focused before tabbing to it");
-  // We will need a third Tab press to get to the Preferences item.
-  sendKey("TAB");
-
-  await SimpleTest.promiseWaitForCondition(() => preferencesItem.matches(":focus"),
-    "waiting for preferencesItem to get focus");
-  ok(preferencesItem.matches(":focus"), ".menuitem-preferences should be focused after tabbing to it");
-
   let openPreferencesEvent = null;
-  is(false, menu.hidden, "menu should be visible before pressing 'space' on .menuitem-preferences");
+  ok(!menu.hidden, "menu should be visible before pressing 'space' on .menuitem-preferences");
   window.addEventListener("AboutLoginsOpenPreferences", event => openPreferencesEvent = event);
   sendKey("SPACE");
   ok(openPreferencesEvent, "AboutLoginsOpenPreferences event should be dispatched after pressing 'space' on .menuitem-preferences");
-  is(true, menu.hidden, "menu should be hidden after pressing 'space' on .menuitem-preferences");
+  ok(menu.hidden, "menu should be hidden after pressing 'space' on .menuitem-preferences");
 });
 </script>
 
 </body>
 </html>
deleted file mode 100644
--- a/browser/components/aboutlogins/tests/chrome/test_reflected_fluent_element.html
+++ /dev/null
@@ -1,117 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<!--
-Test the reflected-fluent-element component
--->
-<head>
-  <meta charset="utf-8">
-  <title>Test the reflected-fluent-element component</title>
-  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
-  <script src="aboutlogins_common.js"></script>
-
-  <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
-</head>
-<body>
-  <p id="display">
-  </p>
-<div id="content" style="display: none">
-  <iframe id="templateFrame" src="chrome://browser/content/aboutlogins/aboutLogins.html"
-          sandbox="allow-same-origin"></iframe>
-</div>
-<pre id="test">
-</pre>
-<script>
-/** Test the reflected-fluent-element component **/
-
-const TEST_STRINGS = {
-  loginFilter: {
-    placeholder: "Sample placeholder",
-  },
-  loginItem: {
-    "cancel-button": "Cancel",
-    "delete-button": "Delete",
-    "origin-label": "Website Address",
-    "password-label": "Password",
-    "save-changes-button": "Save Changes",
-    // See stubFluentL10n for the following three
-    "time-created": "",
-    "time-changed": "",
-    "time-used": "",
-    "username-label": "Username",
-  },
-};
-
-let gLoginFilter;
-let gLoginItem;
-add_task(async function setup() {
-  stubFluentL10n({
-    "time-created": "timeCreated",
-    "time-changed": "timeChanged",
-    "time-used": "timeUsed",
-  });
-
-  let displayEl = document.getElementById("display");
-
-  // Create and append the login-filter element before its template
-  // is cloned the custom element defined.
-  gLoginFilter = document.createElement("login-filter");
-  gLoginFilter.setAttribute("placeholder", TEST_STRINGS.loginFilter.placeholder);
-  displayEl.appendChild(gLoginFilter);
-
-  // ... and do the same with the login-item.
-  gLoginItem = document.createElement("login-item");
-  for (let attrKey of Object.keys(TEST_STRINGS.loginItem)) {
-    gLoginItem.setAttribute(attrKey, TEST_STRINGS.loginItem[attrKey]);
-  }
-  displayEl.appendChild(gLoginItem);
-
-  let templateFrame = document.getElementById("templateFrame");
-  importDependencies(templateFrame, displayEl);
-
-  // The script needs to be inserted after the element and template are appended
-  // to match the environment of the locale text being applied before the custom
-  // element is defined.
-  for (let scriptSrc of ["login-filter.js", "login-item.js", "login-list.js"]) {
-    let scriptEl = document.createElement("script");
-    scriptEl.setAttribute("src", `chrome://browser/content/aboutlogins/components/${scriptSrc}`);
-    scriptEl.setAttribute("type", "module");
-    document.head.appendChild(scriptEl);
-  }
-});
-
-add_task(async function test_placeholder_on_login_filter() {
-  ok(gLoginFilter, "loginFilter exists");
-  await SimpleTest.promiseWaitForCondition(() => !!gLoginFilter.shadowRoot, "Wait for shadowRoot");
-  is(gLoginFilter.shadowRoot.querySelector("input").placeholder,
-     TEST_STRINGS.loginFilter.placeholder,
-     "Placeholder text should be present when set before the element is defined");
-});
-
-add_task(async function test_login_item() {
-  ok(gLoginItem, "loginItem exists");
-  await SimpleTest.promiseWaitForCondition(() => !!gLoginItem.shadowRoot, "Wait for shadowRoot");
-
-  for (let attrKey of Object.keys(TEST_STRINGS.loginItem)) {
-    let selector = "." + attrKey;
-    is(gLoginItem.shadowRoot.querySelector(selector).textContent,
-       TEST_STRINGS.loginItem[attrKey],
-       selector + " textContent should be present when set before the element is defined");
-  }
-});
-
-add_task(async function test_attribute_changed_callback() {
-  let displayEl = document.getElementById("display");
-  let loginList = document.createElement("login-list");
-  displayEl.appendChild(loginList);
-  await SimpleTest.promiseWaitForCondition(() => !!loginList.shadowRoot, "Wait for element to get templated");
-
-  loginList.setAttribute("count", "1234");
-  await SimpleTest.promiseWaitForCondition(() => loginList.shadowRoot.querySelector(".count").textContent.includes("1234"),
-                                           "Wait for text to get localized");
-  ok(loginList.shadowRoot.querySelector(".count").textContent.includes("1234"),
-     "The count attribute should be inherited by the .count span");
-});
-</script>
-
-</body>
-</html>
--- a/browser/components/controlcenter/content/identityPanel.inc.xul
+++ b/browser/components/controlcenter/content/identityPanel.inc.xul
@@ -341,13 +341,13 @@
         <vbox id="identity-popup-breakageReportView-footer"
               class="panel-footer">
           <button id="identity-popup-breakageReportView-cancel"
                   label="&contentBlocking.breakageReportView.cancel.label;"
                   oncommand="ContentBlocking.backToMainView();"/>
           <button id="identity-popup-breakageReportView-submit"
                   default="true"
                   label="&contentBlocking.breakageReportView.sendReport.label;"
-                  oncommand="ContentBlocking.submitBreakageReport();"/>
+                  oncommand="ContentBlocking.onSubmitBreakageReportClicked();"/>
         </vbox>
     </panelview>
   </panelmultiview>
 </panel>
--- a/browser/components/controlcenter/content/protectionsPanel.inc.xul
+++ b/browser/components/controlcenter/content/protectionsPanel.inc.xul
@@ -10,32 +10,41 @@
        onpopupshown="gProtectionsHandler.onPopupShown(event);"
        onpopuphidden="gProtectionsHandler.onPopupHidden(event);"
        orient="vertical">
 
   <panelmultiview id="protections-popup-multiView"
                   mainViewId="protections-popup-mainView">
     <panelview id="protections-popup-mainView"
                descriptionheightworkaround="true">
-      <vbox id="protections-popup-mainView-panel-header">
-        <label>
+      <hbox id="protections-popup-mainView-panel-header"
+            onclick="gProtectionsHandler.onHeaderClicked(event);">
+        <label id="protections-popup-main-header-label">
           <html:span id="protections-popup-mainView-panel-header-span"/>
         </label>
-      </vbox>
+        <description id="protections-popup-toast-panel-tp-on-desc">Enhanced Tracking Protection is ON for this site</description>
+        <description id="protections-popup-toast-panel-tp-off-desc">Enhanced Tracking Protection is OFF for this site</description>
+      </hbox>
 
       <hbox id="protections-popup-tp-switch-section" class="identity-popup-section">
-        <vbox id="protections-popup-tp-switch-label-box" flex="1">
-          <label id="protections-popup-tp-switch-on-header">Tracking protection is ON for this site</label>
-          <label id="protections-popup-tp-switch-off-header">Tracking protection is OFF for this site</label>
-          <label id="protections-popup-tp-switch-breakage-link" class="text-link">Site not working?</label>
+        <vbox class="protections-popup-tp-switch-label-box" flex="1">
+          <label class="protections-popup-tp-switch-on-header"
+                 hidden="true">Tracking protection is ON for this site</label>
+          <label class="protections-popup-tp-switch-off-header"
+                 hidden="true">Tracking protection is OFF for this site</label>
+          <label id="protections-popup-tp-switch-breakage-link"
+                 class="text-link"
+                 onclick="gProtectionsHandler.showSiteNotWorkingView();"
+                 hidden="true">Site not working?</label>
         </vbox>
-        <vbox id="protections-popup-tp-switch-box">
+        <vbox class="protections-popup-tp-switch-box">
           <toolbarbutton id="protections-popup-tp-switch"
-              enabled="false"
-              oncommand="gProtectionsHandler.onTPSwitchCommand();" />
+                         class="protections-popup-tp-switch"
+                         enabled="false"
+                         oncommand="gProtectionsHandler.onTPSwitchCommand();" />
         </vbox>
       </hbox>
 
       <vbox id="protections-popup-settings-section"
             class="identity-popup-section">
         <toolbarbutton id="protections-popup-settings-button"
                        oncommand="ContentBlocking.openPreferences();">
           <image class="protection-settings-icon"/>
@@ -47,10 +56,82 @@
         <description id="protections-popup-trackers-blocked-counter-description"
                      flex="1"/>
         <label id="protections-popup-show-full-report-link"
                is="text-link"
                useoriginprincipal="true"
                href="about:protections">Show Full Report</label>
       </hbox>
     </panelview>
+
+    <!-- Site Not Working? SubView -->
+    <panelview id="protections-popup-siteNotWorkingView"
+               title="Site Not Working?"
+               descriptionheightworkaround="true"
+               flex="1">
+        <hbox id="protections-popup-siteNotWorkingView-header">
+          <vbox class="protections-popup-tp-switch-label-box" flex="1">
+            <label class="protections-popup-tp-switch-on-header"
+              hidden="true">Tracking protection is ON for this site</label>
+            <label class="protections-popup-tp-switch-off-header"
+              hidden="true">Tracking protection is OFF for this site</label>
+          </vbox>
+          <vbox class="protections-popup-tp-switch-box">
+            <toolbarbutton id="protections-popup-siteNotWorking-tp-switch"
+                           class="protections-popup-tp-switch"
+                           enabled="false"
+                           oncommand="gProtectionsHandler.onTPSwitchCommand();" />
+          </vbox>
+        </hbox>
+        <vbox id="protections-popup-siteNotWorkingView-body" flex="1">
+          <label>Turn off Tracking Protection if you're having issues with:</label>
+          <label>
+            <html:ul id="protections-popup-siteNotWorkingView-body-issue-list">
+              <html:li>Log in fields</html:li>
+              <html:li>Forms</html:li>
+              <html:li>Payments</html:li>
+              <html:li>Comments</html:li>
+              <html:li>Videos</html:li>
+            </html:ul>
+          </label>
+        </vbox>
+        <hbox id="protections-popup-siteNotWorkingView-footer"
+              class="panel-footer">
+          <label id="protections-popup-siteNotWorkingView-siteStillBroken" flex="1">Site Still Broken?</label>
+          <label id="protections-popup-siteNotWorkingView-sendReport"
+            onclick="gProtectionsHandler.showSendReportView();"
+            class="text-link">Send Report</label>
+        </hbox>
+    </panelview>
+
+
+    <!-- Send Report SubView -->
+    <panelview id="protections-popup-sendReportView"
+               title="Send Report"
+               descriptionheightworkaround="true">
+        <vbox id="protections-popup-sendReportView-heading">
+          <description>&contentBlocking.breakageReportView2.description;</description>
+          <label id="protections-popup-sendReportView-learn-more"
+                 is="text-link">&contentBlocking.breakageReportView.learnMore;</label>
+        </vbox>
+        <vbox id="protections-popup-sendReportView-body" class="panel-view-body-unscrollable">
+          <vbox class="protections-popup-sendReportView-collection-section">
+            <label>&contentBlocking.breakageReportView.collection.url.label;</label>
+            <html:input readonly="readonly" id="protections-popup-sendReportView-collection-url"/>
+          </vbox>
+          <vbox class="protections-popup-sendReportView-collection-section">
+            <label>&contentBlocking.breakageReportView.collection.comments.label;</label>
+            <html:textarea id="protections-popup-sendReportView-collection-comments"/>
+          </vbox>
+        </vbox>
+        <vbox id="protections-popup-sendReportView-footer"
+              class="panel-footer">
+          <button id="protections-popup-sendReportView-cancel"
+                  label="&contentBlocking.breakageReportView.cancel.label;"
+                  oncommand="gProtectionsHandler._protectionsPopupMultiView.goBack();"/>
+          <button id="protections-popup-sendReportView-submit"
+                  default="true"
+                  label="&contentBlocking.breakageReportView.sendReport.label;"
+                  oncommand="gProtectionsHandler.onSendReportClicked();"/>
+        </vbox>
+    </panelview>
   </panelmultiview>
 </panel>
--- a/browser/components/extensions/ext-browser.json
+++ b/browser/components/extensions/ext-browser.json
@@ -193,16 +193,24 @@
     "url": "chrome://browser/content/parent/ext-tabs.js",
     "schema": "chrome://browser/content/schemas/tabs.json",
     "scopes": ["addon_parent"],
     "events": ["update", "disable"],
     "paths": [
       ["tabs"]
     ]
   },
+  "topSites": {
+    "url": "chrome://extensions/content/parent/ext-topSites.js",
+    "schema": "chrome://extensions/content/schemas/top_sites.json",
+    "scopes": ["addon_parent"],
+    "paths": [
+      ["topSites"]
+    ]
+  },
   "urlbar": {
     "url": "chrome://browser/content/parent/ext-urlbar.js",
     "schema": "chrome://browser/content/schemas/urlbar.json",
     "scopes": ["addon_parent"],
     "paths": [
       ["urlbar"]
     ]
   },
--- a/browser/components/migration/tests/marionette/test_refresh_firefox.py
+++ b/browser/components/migration/tests/marionette/test_refresh_firefox.py
@@ -1,8 +1,9 @@
+from __future__ import absolute_import, print_function
 import os
 import shutil
 import time
 
 from marionette_harness import MarionetteTestCase
 from marionette_driver.errors import NoAlertPresentException
 
 
--- a/browser/components/newtab/.eslintignore
+++ b/browser/components/newtab/.eslintignore
@@ -1,4 +1,3 @@
 data/
 logs/
-prerendered/
 vendor/
--- a/browser/components/newtab/.eslintrc.js
+++ b/browser/components/newtab/.eslintrc.js
@@ -40,16 +40,17 @@ module.exports = {
     "NewTabPagePreloading": true,
   },
   "overrides": [
     {
       // These files use fluent-dom to insert content
       "files": [
         "content-src/asrouter/templates/OnboardingMessage/**",
         "content-src/asrouter/templates/Trailhead/**",
+        "content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx",
         "content-src/components/TopSites/**",
         "content-src/components/MoreRecommendations/MoreRecommendations.jsx",
         "content-src/components/CollapsibleSection/CollapsibleSection.jsx"
       ],
       "rules": {
         "jsx-a11y/anchor-has-content": 0,
         "jsx-a11y/heading-has-content": 0,
       }
--- a/browser/components/newtab/.mcignore
+++ b/browser/components/newtab/.mcignore
@@ -1,18 +1,18 @@
 npm-debug.log
 .DS_Store
 *.sw[po]
 *.xpi
 *.pyc
 *.update.rdf
 .gitignore
+.eslintcache
 
 /.git/
-/data/locales.json
 /dist/
 /logs/
 /node_modules/
 
 # ignore README since it's GitHub specific
 /README.md
 
 # also ignores ping centre tests
--- a/browser/components/newtab/AboutNewTabService.jsm
+++ b/browser/components/newtab/AboutNewTabService.jsm
@@ -9,38 +9,32 @@
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 const {E10SUtils} = ChromeUtils.import("resource://gre/modules/E10SUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "AboutNewTab",
                                "resource:///modules/AboutNewTab.jsm");
 
 const TOPIC_APP_QUIT = "quit-application-granted";
-const TOPIC_LOCALES_CHANGE = "intl:app-locales-changed";
 const TOPIC_CONTENT_DOCUMENT_INTERACTIVE = "content-document-interactive";
 
-// Automated tests ensure packaged locales are in this list. Copied output of:
-// https://github.com/mozilla/activity-stream/blob/master/bin/render-activity-stream-html.js
-const ACTIVITY_STREAM_BCP47 = "en-US ach an ar ast az be bg bn br bs ca cak crh cs cy da de dsb el en-CA en-GB eo es-AR es-CL es-ES es-MX et eu fa ff fi fr fy-NL ga-IE gd gl gn gu-IN he hi-IN hr hsb hu hy-AM ia id is it ja ja-JP-macos ka kab kk km kn ko lij lo lt ltg lv mk mr ms my nb-NO ne-NP nl nn-NO oc pa-IN pl pt-BR pt-PT rm ro ru si sk sl sq sr sv-SE ta te th tl tr trs uk ur uz vi zh-CN zh-TW".split(" ");
-
 const ABOUT_URL = "about:newtab";
 const BASE_URL = "resource://activity-stream/";
 const ACTIVITY_STREAM_PAGES = new Set(["home", "newtab", "welcome"]);
 
 const IS_MAIN_PROCESS = Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
 const IS_PRIVILEGED_PROCESS = Services.appinfo.remoteType === E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
 
 const IS_RELEASE_OR_BETA = AppConstants.RELEASE_OR_BETA;
 
 const PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS = "browser.tabs.remote.separatePrivilegedContentProcess";
 const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug";
 
 function AboutNewTabService() {
   Services.obs.addObserver(this, TOPIC_APP_QUIT);
-  Services.obs.addObserver(this, TOPIC_LOCALES_CHANGE);
   Services.prefs.addObserver(PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS, this);
   if (!IS_RELEASE_OR_BETA) {
     Services.prefs.addObserver(PREF_ACTIVITY_STREAM_DEBUG, this);
   }
 
   // More initialization happens here
   this.toggleActivityStream(true);
   this.initialized = true;
@@ -66,55 +60,52 @@ function AboutNewTabService() {
  *
  * When not overridden, the default URL emitted by the service is "about:newtab".
  * When overridden, it returns the overriden URL.
  *
  * 2. Redirector Access:
  *
  * When the URL loaded is about:newtab, the default behavior, or when entered in the
  * URL bar, the redirector is hit. The service is then called to return the
- * appropriate activity stream url based on prefs and locales.
+ * appropriate activity stream url based on prefs.
  *
  * NOTE: "about:newtab" will always result in a default newtab page, and never an overridden URL.
  *
  * Access patterns:
  *
  * The behavior is different when accessing the service via browser chrome or via redirector
  * largely to maintain compatibility with expectations of add-on developers.
  *
  * Loading a chrome resource, or an about: URL in the redirector with either the
  * LOAD_NORMAL or LOAD_REPLACE flags yield unexpected behaviors, so a roundtrip
  * to the redirector from browser chrome is avoided.
  */
 AboutNewTabService.prototype = {
 
   _newTabURL: ABOUT_URL,
   _activityStreamEnabled: false,
-  _activityStreamPath: "",
   _activityStreamDebug: false,
   _privilegedAboutContentProcess: false,
   _overridden: false,
   willNotifyUser: false,
 
   classID: Components.ID("{dfcd2adc-7867-4d3a-ba70-17501f208142}"),
   QueryInterface: ChromeUtils.generateQI([
     Ci.nsIAboutNewTabService,
     Ci.nsIObserver,
   ]),
 
   observe(subject, topic, data) {
     switch (topic) {
       case "nsPref:changed":
         if (data === PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS) {
           this._privilegedAboutContentProcess = Services.prefs.getBoolPref(PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS);
-          this.updatePrerenderedPath();
           this.notifyChange();
         } else if (!IS_RELEASE_OR_BETA && data === PREF_ACTIVITY_STREAM_DEBUG) {
           this._activityStreamDebug = Services.prefs.getBoolPref(PREF_ACTIVITY_STREAM_DEBUG, false);
-          this.updatePrerenderedPath();
           this.notifyChange();
         }
         break;
       case TOPIC_CONTENT_DOCUMENT_INTERACTIVE: {
         const win = subject.defaultView;
 
         // It seems like "content-document-interactive" is triggered multiple
         // times for a single window. The first event always seems to be an
@@ -140,20 +131,18 @@ AboutNewTabService.prototype = {
 
           // This list must match any similar ones in render-activity-stream-html.js.
           const scripts = [
             "chrome://browser/content/contentSearchUI.js",
             "chrome://browser/content/contentTheme.js",
             `${BASE_URL}vendor/react${debugString}.js`,
             `${BASE_URL}vendor/react-dom${debugString}.js`,
             `${BASE_URL}vendor/prop-types.js`,
-            `${BASE_URL}vendor/react-intl.js`,
             `${BASE_URL}vendor/redux.js`,
             `${BASE_URL}vendor/react-redux.js`,
-            `${BASE_URL}prerendered/${this.activityStreamLocale}/activity-stream-strings.js`,
             `${BASE_URL}data/content/activity-stream.bundle.js`,
           ];
 
           for (let script of scripts) {
             Services.scriptloader.loadSubScript(script, win); // Synchronous call
           }
         };
         subject.addEventListener("DOMContentLoaded", onLoaded, {once: true});
@@ -170,20 +159,16 @@ AboutNewTabService.prototype = {
       case TOPIC_APP_QUIT:
         this.uninit();
         if (IS_MAIN_PROCESS) {
           AboutNewTab.uninit();
         } else if (IS_PRIVILEGED_PROCESS) {
           Services.obs.removeObserver(this, TOPIC_CONTENT_DOCUMENT_INTERACTIVE);
         }
         break;
-      case TOPIC_LOCALES_CHANGE:
-        this.updatePrerenderedPath();
-        this.notifyChange();
-        break;
     }
   },
 
   notifyChange() {
     Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
   },
 
   /**
@@ -205,44 +190,33 @@ AboutNewTabService.prototype = {
       this._activityStreamEnabled = true;
     } else {
       this._activityStreamEnabled = false;
     }
     this._privilegedAboutContentProcess = Services.prefs.getBoolPref(PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS);
     if (!IS_RELEASE_OR_BETA) {
       this._activityStreamDebug = Services.prefs.getBoolPref(PREF_ACTIVITY_STREAM_DEBUG, false);
     }
-    this.updatePrerenderedPath();
     this._newtabURL = ABOUT_URL;
     return true;
   },
 
-  /**
-   * Figure out what path under prerendered to use based on current state.
-   */
-  updatePrerenderedPath() {
-    // Debug files are specially packaged in a non-localized directory, but with
-    // dynamic script loading, localized debug is supported.
-    this._activityStreamPath = `${this._activityStreamDebug &&
-      !this._privilegedAboutContentProcess ? "static" : this.activityStreamLocale}/`;
-  },
-
   /*
    * Returns the default URL.
    *
-   * This URL depends on various activity stream prefs and locales. Overriding
+   * This URL depends on various activity stream prefs. Overriding
    * the newtab page has no effect on the result of this function.
    */
   get defaultURL() {
     // Generate the desired activity stream resource depending on state, e.g.,
-    // resource://activity-stream/prerendered/ar/activity-stream.html
-    // resource://activity-stream/prerendered/static/activity-stream-debug.html
+    // "resource://activity-stream/prerendered/activity-stream.html"
+    // "resource://activity-stream/prerendered/activity-stream-debug.html"
+    // "resource://activity-stream/prerendered/activity-stream-noscripts.html"
     return [
       "resource://activity-stream/prerendered/",
-      this._activityStreamPath,
       "activity-stream",
       // Debug version loads dev scripts but noscripts separately loads scripts
       this._activityStreamDebug && !this._privilegedAboutContentProcess ? "-debug" : "",
       this._privilegedAboutContentProcess ? "-noscripts" : "",
       ".html",
     ].join("");
   },
 
@@ -282,31 +256,16 @@ AboutNewTabService.prototype = {
   get activityStreamEnabled() {
     return this._activityStreamEnabled;
   },
 
   get activityStreamDebug() {
     return this._activityStreamDebug;
   },
 
-  get activityStreamLocale() {
-    // Pick the best available locale to match the app locales
-    return Services.locale.negotiateLanguages(
-      // Fix up incorrect BCP47 that are actually lang tags as a workaround for
-      // bug 1479606 returning the wrong values in the content process
-      Services.locale.appLocalesAsBCP47.map(l => l.replace(/^(ja-JP-mac)$/, "$1os")),
-      ACTIVITY_STREAM_BCP47,
-      // defaultLocale's strings aren't necessarily packaged, but en-US' are
-      "en-US",
-      Services.locale.langNegStrategyLookup
-    // Convert the BCP47 to lang tag, which is what is used in our paths, as a
-    // workaround for bug 1478930 negotiating incorrectly with lang tags
-    )[0].replace(/^(ja-JP-mac)os$/, "$1");
-  },
-
   resetNewTabURL() {
     this._overridden = false;
     this._newTabURL = ABOUT_URL;
     this.toggleActivityStream(true, true);
     this.notifyChange();
   },
 
   maybeRecordTopsitesPainted(timestamp) {
@@ -323,17 +282,16 @@ AboutNewTabService.prototype = {
     this.alreadyRecordedTopsitesPainted = true;
   },
 
   uninit() {
     if (!this.initialized) {
       return;
     }
     Services.obs.removeObserver(this, TOPIC_APP_QUIT);
-    Services.obs.removeObserver(this, TOPIC_LOCALES_CHANGE);
     Services.prefs.removeObserver(PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS, this);
     if (!IS_RELEASE_OR_BETA) {
       Services.prefs.removeObserver(PREF_ACTIVITY_STREAM_DEBUG, this);
     }
     this.initialized = false;
   },
 };
 
deleted file mode 100644
--- a/browser/components/newtab/bin/locales.js
+++ /dev/null
@@ -1,112 +0,0 @@
-if (!process.env.npm_lifecycle_event) {
-  throw Error("You should only run this from npm script contexts");
-}
-
-exports.DEFAULT_LOCALE = process.env.npm_package_config_default_locale;
-
-exports.LOCALES_SOURCE_DIRECTORY = process.env.npm_package_config_locales_dir;
-
-// This locales list is to find any similar locales that we can reuse strings
-// instead of falling back to the default, e.g., use bn-BD strings for bn-IN.
-// https://hg.mozilla.org/mozilla-central/file/tip/browser/locales/l10n.toml
-exports.CENTRAL_LOCALES = [
-  "ach",
-  "af",
-  "an",
-  "ar",
-  "ast",
-  "az",
-  "be",
-  "bg",
-  "bn",
-  "br",
-  "bs",
-  "ca",
-  "cak",
-  "crh",
-  "cs",
-  "cy",
-  "da",
-  "de",
-  "dsb",
-  "el",
-  "en-CA",
-  "en-GB",
-  "eo",
-  "es-AR",
-  "es-CL",
-  "es-ES",
-  "es-MX",
-  "et",
-  "eu",
-  "fa",
-  "ff",
-  "fi",
-  "fr",
-  "fy-NL",
-  "ga-IE",
-  "gd",
-  "gl",
-  "gn",
-  "gu-IN",
-  "he",
-  "hi-IN",
-  "hr",
-  "hsb",
-  "hu",
-  "hy-AM",
-  "ia",
-  "id",
-  "is",
-  "it",
-  "ja",
-  "ja-JP-mac",
-  "ka",
-  "kab",
-  "kk",
-  "km",
-  "kn",
-  "ko",
-  "lij",
-  "lo",
-  "lt",
-  "ltg",
-  "lv",
-  "mk",
-  "mr",
-  "ms",
-  "my",
-  "nb-NO",
-  "ne-NP",
-  "nl",
-  "nn-NO",
-  "oc",
-  "pa-IN",
-  "pl",
-  "pt-BR",
-  "pt-PT",
-  "rm",
-  "ro",
-  "ru",
-  "si",
-  "sk",
-  "sl",
-  "son",
-  "sq",
-  "sr",
-  "sv-SE",
-  "ta",
-  "te",
-  "th",
-  "tl",
-  "tr",
-  "trs",
-  "uk",
-  "ur",
-  "uz",
-  "vi",
-  "wo",
-  "xh",
-  "zh-CN",
-  "zh-TW",
-];
--- a/browser/components/newtab/bin/render-activity-stream-html.js
+++ b/browser/components/newtab/bin/render-activity-stream-html.js
@@ -1,93 +1,48 @@
  /* eslint-disable no-console */
 const fs = require("fs");
 const {mkdir} = require("shelljs");
 const path = require("path");
 
-const {CENTRAL_LOCALES, DEFAULT_LOCALE} = require("./locales");
-
 // Note: DEFAULT_OPTIONS.baseUrl should match BASE_URL in aboutNewTabService.js
 //       in mozilla-central.
 const DEFAULT_OPTIONS = {
   addonPath: "..",
   baseUrl: "resource://activity-stream/",
 };
 
-// Locales that should be displayed RTL
-const RTL_LIST = ["ar", "he", "fa", "ur"];
-
-/**
- * Get the language part of the locale.
- */
-function getLanguage(locale) {
-  return locale.split("-")[0];
-}
-
-/**
- * Get the best strings for a single provided locale using similar locales and
- * DEFAULT_LOCALE as fallbacks.
- */
-function getStrings(locale, allStrings) {
-  const availableLocales = Object.keys(allStrings);
-
-  const language = getLanguage(locale);
-  const similarLocales = availableLocales.filter(other =>
-    other !== locale && getLanguage(other) === language);
-
-  // Rank locales from least desired to most desired
-  const localeFallbacks = [DEFAULT_LOCALE, ...similarLocales, locale];
-
-  // Get strings from each locale replacing with those from more desired ones
-  const desired = Object.assign({}, ...localeFallbacks.map(l => allStrings[l]));
-
-  // Only include strings that are currently used (defined by default locale)
-  return Object.assign({}, ...Object.keys(allStrings[DEFAULT_LOCALE]).map(
-    key => ({[key]: desired[key]})));
-}
-
-/**
- * Get the text direction of the locale.
- */
-function getTextDirection(locale) {
-  return RTL_LIST.includes(locale.split("-")[0]) ? "rtl" : "ltr";
-}
-
 /**
  * templateHTML - Generates HTML for activity stream, given some options and
  * prerendered HTML if necessary.
  *
  * @param  {obj} options
- *         {str} options.locale         The locale to render in lang="" attribute
- *         {str} options.direction      The language direction to render in dir="" attribute
  *         {str} options.baseUrl        The base URL for all local assets
  *         {bool} options.debug         Should we use dev versions of JS libraries?
  *         {bool} options.noscripts     Should we include scripts in the prerendered files?
  * @return {str}         An HTML document as a string
  */
 function templateHTML(options) {
   const debugString = options.debug ? "-dev" : "";
   const scripts = [
     "chrome://browser/content/contentSearchUI.js",
     "chrome://browser/content/contentTheme.js",
     `${options.baseUrl}vendor/react${debugString}.js`,
     `${options.baseUrl}vendor/react-dom${debugString}.js`,
     `${options.baseUrl}vendor/prop-types.js`,
-    `${options.baseUrl}vendor/react-intl.js`,
     `${options.baseUrl}vendor/redux.js`,
     `${options.baseUrl}vendor/react-redux.js`,
-    `${options.baseUrl}prerendered/${options.locale}/activity-stream-strings.js`,
     `${options.baseUrl}data/content/activity-stream.bundle.js`,
   ];
 
   // Add spacing and script tags
   const scriptRender = `\n${scripts.map(script => `    <script src="${script}"></script>`).join("\n")}`;
 
   return `<!doctype html>
-<html lang="${options.locale}" dir="${options.direction}">
+<html>
   <head>
     <meta charset="utf-8">
     <meta http-equiv="Content-Security-Policy" content="default-src 'none'; object-src 'none'; script-src resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';">
     <title data-l10n-id="newtab-page-title"></title>
     <link rel="icon" type="image/png" href="chrome://branding/content/icon32.png"/>
     <link rel="localization" href="browser/branding/brandings.ftl" />
     <link rel="localization" href="browser/newtab/newtab.ftl" />
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
@@ -98,58 +53,33 @@ function templateHTML(options) {
     <div id="root"></div>
     <div id="footer-asrouter-container" role="presentation"></div>${options.noscripts ? "" : scriptRender}
   </body>
 </html>
 `;
 }
 
 /**
- * templateJs - Generates a js file that passes the initial state of the prerendered
- * DOM to the React version. This is necessary to ensure the checksum matches when
- * React mounts so that it can attach to the prerendered elements instead of blowing
- * them away.
- *
- * Note that this may no longer be necessary in React 16 and we should review whether
- * it is still necessary.
- *
- * @param  {string} name The name of the global to expose
- * @param  {string} desc Extra description to include in a js comment
- * @param  {obj}   state The data to expose as a window global
- * @return {str}         The js file as a string
- */
-function templateJs(name, desc, state) {
-  return `// Note - this is a generated ${desc} file.
-window.${name} = ${JSON.stringify(state, null, 2)};
-`;
-}
-
-/**
  * writeFiles - Writes to the desired files the result of a template given
  * various prerendered data and options.
  *
- * @param {string} name          Something to identify in the console
  * @param {string} destPath      Path to write the files to
  * @param {Map}    filesMap      Mapping of a string file name to templater
  * @param {Object} options       Various options for the templater
  */
-function writeFiles(name, destPath, filesMap, options) {
+function writeFiles(destPath, filesMap, options) {
   for (const [file, templater] of filesMap) {
+    console.log("\x1b[32m", `✓ ${file}`, "\x1b[0m");
     fs.writeFileSync(path.join(destPath, file), templater({options}));
   }
-  console.log("\x1b[32m", `✓ ${name}`, "\x1b[0m");
 }
 
 const STATIC_FILES = new Map([
-  ["activity-stream-debug.html", ({options}) => templateHTML(options)],
-]);
-
-const LOCALIZED_FILES = new Map([
-  ["activity-stream-strings.js", ({options: {locale, strings}}) => templateJs("gActivityStreamStrings", locale, strings)],
   ["activity-stream.html", ({options}) => templateHTML(options)],
+  ["activity-stream-debug.html", ({options}) => templateHTML(Object.assign({}, options, {debug: true}))],
   ["activity-stream-noscripts.html", ({options}) => templateHTML(Object.assign({}, options, {noscripts: true}))],
 ]);
 
 /**
  * main - Parses command line arguments, generates html and js with templates,
  *        and writes files to their specified locations.
  */
 function main() { // eslint-disable-line max-statements
@@ -158,79 +88,18 @@ function main() { // eslint-disable-line
   // process.argv are paths
   const args = require("minimist")(process.argv.slice(2), {
     alias: {
       addonPath: "a",
       baseUrl: "b",
     },
   });
 
-  const baseOptions = Object.assign({debug: false}, DEFAULT_OPTIONS, args || {});
-  const addonPath = path.resolve(__dirname, baseOptions.addonPath);
-  const allStrings = require(`${baseOptions.addonPath}/data/locales.json`);
-  const extraLocales = Object.keys(allStrings).filter(locale =>
-    locale !== DEFAULT_LOCALE && !CENTRAL_LOCALES.includes(locale));
-
+  const options = Object.assign({debug: false}, DEFAULT_OPTIONS, args || {});
+  const addonPath = path.resolve(__dirname, options.addonPath);
   const prerenderedPath = path.join(addonPath, "prerendered");
-  console.log(`Writing prerendered files to individual directories under ${prerenderedPath}:`);
-
-  // Save default locale's strings to compare against other locales' strings
-  let defaultStrings;
-  let langStrings;
-  const isSubset = (strings, existing) => existing &&
-    Object.keys(strings).every(key => strings[key] === existing[key]);
-
-  // Process the default locale first then all the ones from mozilla-central
-  const localizedLocales = [];
-  const skippedLocales = [];
-  for (const locale of [DEFAULT_LOCALE, ...CENTRAL_LOCALES]) {
-    // Skip the locale if it would have resulted in duplicate packaged files
-    const strings = getStrings(locale, allStrings);
-    if (isSubset(strings, defaultStrings) || isSubset(strings, langStrings)) {
-      skippedLocales.push(locale);
-      continue;
-    }
-
-    const options = Object.assign({}, baseOptions, {
-      direction: getTextDirection(locale),
-      locale,
-      strings,
-    });
+  console.log(`Writing prerendered files to ${prerenderedPath}:`);
 
-    // Put locale-specific files in their own directory
-    const localePath = path.join(prerenderedPath, "locales", locale);
-    mkdir("-p", localePath);
-    writeFiles(locale, localePath, LOCALIZED_FILES, options);
-
-    // Only write static files once for the default locale
-    if (locale === DEFAULT_LOCALE) {
-      const staticPath = path.join(prerenderedPath, "static");
-      mkdir("-p", staticPath);
-      writeFiles(`${locale} (static)`, staticPath, STATIC_FILES,
-        Object.assign({}, options, {debug: true}));
-
-      // Save the default strings to compare against other locales' strings
-      defaultStrings = strings;
-    }
-
-    // Save the language's strings to maybe reuse for the next similar locales
-    if (getLanguage(locale) === locale) {
-      langStrings = strings;
-    }
-
-    localizedLocales.push(locale);
-  }
-
-  if (skippedLocales.length) {
-    console.log("\x1b[33m", `Skipped the following locales because they use the same strings as ${DEFAULT_LOCALE} or its language locale: ${skippedLocales.join(", ")}`, "\x1b[0m");
-  }
-  if (extraLocales.length) {
-    console.log("\x1b[33m", `Skipped the following locales because they are not in CENTRAL_LOCALES: ${extraLocales.join(", ")}`, "\x1b[0m");
-  }
-
-  // Convert ja-JP-mac lang tag to ja-JP-macos bcp47 to work around bug 1478930
-  const bcp47String = localizedLocales.join(" ").replace(/(ja-JP-mac)/, "$1os");
-
-  // Provide some help to copy/paste locales if tests are failing
-  console.log(`\nIf aboutNewTabService tests are failing for unexpected locales, make sure its list is updated:\nconst ACTIVITY_STREAM_BCP47 = "${bcp47String}".split(" ");`);
+  mkdir("-p", prerenderedPath);
+  writeFiles(prerenderedPath, STATIC_FILES, options);
 }
 
 main();
deleted file mode 100755
--- a/browser/components/newtab/bin/strings-import.js
+++ /dev/null
@@ -1,106 +0,0 @@
-#! /usr/bin/env node
-"use strict";
-
-/* eslint-disable no-console */
-const fetch = require("node-fetch");
-
-/* globals cd, ls, mkdir, rm, ShellString */
-require("shelljs/global");
-
-const {CENTRAL_LOCALES, DEFAULT_LOCALE, LOCALES_SOURCE_DIRECTORY} = require("./locales");
-const L10N_CENTRAL = "https://hg.mozilla.org/l10n-central";
-const PROPERTIES_PATH = "raw-file/default/browser/chrome/browser/activity-stream/newtab.properties";
-const STRINGS_FILE = "strings.properties";
-
-// Get all the locales in l10n-central
-async function getLocales() {
-  console.log(`Getting locales from ${L10N_CENTRAL}`);
-
-  // Add sub repository locales that mozilla-central builds
-  const locales = [];
-  const unbuilt = [];
-  const subrepos = await (await fetch(`${L10N_CENTRAL}?style=json`)).json();
-  subrepos.entries.forEach(({name}) => {
-    if (CENTRAL_LOCALES.includes(name)) {
-      locales.push(name);
-    } else {
-      unbuilt.push(name);
-    }
-  });
-
-  console.log(`Got ${locales.length} mozilla-central locales: ${locales}`);
-  console.log(`Skipped ${unbuilt.length} unbuilt locales: ${unbuilt}`);
-
-  return locales;
-}
-
-// Pick a different string if the desired one is missing
-const transvision = {};
-async function cherryPickString(locale) {
-  const getTransvision = async string => {
-    if (!transvision[string]) {
-      // eslint-disable-next-line fetch-options/no-fetch-credentials
-      const response = await fetch(`https://transvision.mozfr.org/api/v1/entity/gecko_strings/?id=${string}`);
-      transvision[string] = response.ok ? await response.json() : {};
-    }
-    return transvision[string];
-  };
-  const expectedKey = "section_menu_action_add_search_engine";
-  const expected = await getTransvision(`browser/chrome/browser/activity-stream/newtab.properties:${expectedKey}`);
-  const target = await getTransvision("browser/chrome/browser/search.properties:searchAddFoundEngine2");
-  return !expected[locale] && target[locale] ? `${expectedKey}=${target[locale]}\n` : "";
-}
-
-// Save the properties file to the locale's directory
-async function saveProperties(locale) {
-  // Only save a file if the repository has the file
-  const url = `${L10N_CENTRAL}/${locale}/${PROPERTIES_PATH}`;
-  const response = await fetch(url); // eslint-disable-line fetch-options/no-fetch-credentials
-  if (!response.ok) {
-    // Indicate that this locale didn't save
-    return locale;
-  }
-
-  // Save the file to the right place
-  const text = await response.text();
-  mkdir(locale);
-  cd(locale);
-  // For now, detect if a string is missing to use a different one instead
-  ShellString(text + await cherryPickString(locale)).to(STRINGS_FILE);
-  cd("..");
-
-  // Indicate that we were successful in saving
-  return "";
-}
-
-// Replace and update each locale's strings
-async function updateLocales() {
-  console.log(`Switching to and deleting existing l10n tree under: ${LOCALES_SOURCE_DIRECTORY}`);
-
-  cd(LOCALES_SOURCE_DIRECTORY);
-  ls().forEach(dir => {
-    // Keep the default/source locale as it might have newer strings
-    if (dir !== DEFAULT_LOCALE) {
-      rm("-r", dir);
-    }
-  });
-
-  // Save the properties file for each locale one at a time to avoid too many
-  // parallel connections (resulting in ECONNRESET / socket hang up)
-  const missing = [];
-  for (const locale of await getLocales()) {
-    process.stdout.write(`${locale} `);
-    if (await saveProperties(locale)) {
-      missing.push(locale);
-    }
-  }
-
-  console.log("");
-  console.log(`Skipped ${missing.length} locales without strings: ${missing.sort()}`);
-
-  console.log(`
-Please check the diffs, add/remove files, and then commit the result. Suggested commit message:
-chore(l10n): Update from l10n-central ${new Date()}`);
-}
-
-updateLocales().catch(console.error);
--- a/browser/components/newtab/bin/vendor.js
+++ b/browser/components/newtab/bin/vendor.js
@@ -8,18 +8,16 @@ const path = require("path");
 const filesToVendor = {
   // XXX currently these two licenses are identical.  Perhaps we should check
   // in case that changes at some point in the future.
   "react/LICENSE": "REACT_AND_REACT_DOM_LICENSE",
   "react/umd/react.production.min.js": "react.js",
   "react/umd/react.development.js": "react-dev.js",
   "react-dom/umd/react-dom.production.min.js": "react-dom.js",
   "react-dom/umd/react-dom.development.js": "react-dom-dev.js",
-  "react-intl/LICENSE.md": "REACT_INTL_LICENSE",
-  "react-intl/dist/react-intl.min.js": "react-intl.js",
   "react-redux/LICENSE.md": "REACT_REDUX_LICENSE",
   "react-redux/dist/react-redux.min.js": "react-redux.js",
 };
 
 set("-v"); // Echo all the copy commands so the user can see what's going on
 for (let srcPath of Object.keys(filesToVendor)) {
   cp(path.join("node_modules", srcPath),
     path.join("vendor", filesToVendor[srcPath]));
--- a/browser/components/newtab/content-src/asrouter/asrouter-content.jsx
+++ b/browser/components/newtab/content-src/asrouter/asrouter-content.jsx
@@ -1,9 +1,8 @@
-import {addLocaleData, IntlProvider} from "react-intl";
 import {actionCreators as ac} from "common/Actions.jsm";
 import {OUTGOING_MESSAGE_NAME as AS_GENERAL_OUTGOING_MESSAGE_NAME} from "content-src/lib/init-store";
 import {generateBundles} from "./rich-text-strings";
 import {ImpressionsWrapper} from "./components/ImpressionsWrapper/ImpressionsWrapper";
 import {LocalizationProvider} from "fluent-react";
 import {NEWTAB_DARK_THEME} from "content-src/lib/constants";
 import {OnboardingMessage} from "./templates/OnboardingMessage/OnboardingMessage";
 import React from "react";
@@ -211,21 +210,16 @@ export class ASRouterUISurface extends R
         break;
       case "AS_ROUTER_TARGETING_UPDATE":
         action.data.forEach(id => this.clearMessage(id));
         break;
     }
   }
 
   componentWillMount() {
-    if (global.document) {
-      // Add locale data for StartupOverlay because it uses react-intl
-      addLocaleData(global.document.documentElement.lang);
-    }
-
     const endpoint = ASRouterUtils.getPreviewEndpoint();
     if (endpoint && endpoint.theme === "dark") {
       global.window.dispatchEvent(new CustomEvent("LightweightTheme:Set", {detail: {data: NEWTAB_DARK_THEME}}));
     }
     ASRouterUtils.addListener(this.onMessageFromParent);
 
     // If we are loading about:welcome we want to trigger the onboarding messages
     if (this.props.document && this.props.document.location.href === "about:welcome") {
@@ -283,22 +277,20 @@ export class ASRouterUISurface extends R
     return null;
   }
 
   renderFirstRunOverlay() {
     const {message} = this.state;
     if (message.template === "fxa_overlay") {
       global.document.body.classList.add("fxa");
       return (
-        <IntlProvider locale={global.document.documentElement.lang} messages={global.gActivityStreamStrings}>
-          <StartupOverlay
-            onReady={this.triggerOnboarding}
-            onBlock={this.onDismissById(message.id)}
-            dispatch={this.props.dispatch} />
-        </IntlProvider>
+        <StartupOverlay
+          onReady={this.triggerOnboarding}
+          onBlock={this.onDismissById(message.id)}
+          dispatch={this.props.dispatch} />
       );
     } else if (message.template === "return_to_amo_overlay") {
       global.document.body.classList.add("amo");
       return (
         <LocalizationProvider bundles={generateBundles({"amo_html": message.content.text})}>
           <ReturnToAMO
             {...message}
             UISurface="NEWTAB_OVERLAY"
--- a/browser/components/newtab/content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx
@@ -1,13 +1,18 @@
 import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
-import {FormattedMessage, injectIntl} from "react-intl";
 import {connect} from "react-redux";
 import React from "react";
 
+const FLUENT_FILES = [
+  "branding/brand.ftl",
+  "browser/branding/sync-brand.ftl",
+  "browser/newtab/onboarding.ftl",
+];
+
 export class _StartupOverlay extends React.PureComponent {
   constructor(props) {
     super(props);
     this.onInputChange = this.onInputChange.bind(this);
     this.onSubmit = this.onSubmit.bind(this);
     this.clickSkip = this.clickSkip.bind(this);
     this.initScene = this.initScene.bind(this);
     this.removeOverlay = this.removeOverlay.bind(this);
@@ -38,16 +43,26 @@ export class _StartupOverlay extends Rea
           this.props.dispatch(ac.OnlyToMain({type: at.TELEMETRY_UNDESIRED_EVENT, data: {event: "FXA_METRICS_FETCH_ERROR", value: response.status}}));
         }
       } catch (error) {
         this.props.dispatch(ac.OnlyToMain({type: at.TELEMETRY_UNDESIRED_EVENT, data: {event: "FXA_METRICS_ERROR"}}));
       }
     }
   }
 
+  async componentWillMount() {
+    FLUENT_FILES.forEach(file => {
+      const link = document.head.appendChild(document.createElement("link"));
+      link.href = file;
+      link.rel = "localization";
+    });
+
+    await this.componentWillUpdate(this.props);
+  }
+
   componentDidMount() {
     this.initScene();
   }
 
   initScene() {
     // Timeout to allow the scene to render once before attaching the attribute
     // to trigger the animation.
     setTimeout(() => {
@@ -104,58 +119,59 @@ export class _StartupOverlay extends Rea
 
   render() {
     // When skipping the onboarding tour we show AS but we are still on
     // about:welcome, prop.isFirstrun is true and StartupOverlay is rendered
     if (this.state.overlayRemoved) {
       return null;
     }
 
-    let termsLink = (<a href={`${this.props.fxa_endpoint}/legal/terms?${this.utmParams}`} target="_blank" rel="noopener noreferrer"><FormattedMessage id="firstrun_terms_of_service" /></a>);
-    let privacyLink = (<a href={`${this.props.fxa_endpoint}/legal/privacy?${this.utmParams}`} target="_blank" rel="noopener noreferrer"><FormattedMessage id="firstrun_privacy_notice" /></a>);
-
+ 
     return (
       <div className={`overlay-wrapper ${this.state.show ? "show" : ""}`}>
         <div className="background" />
         <div className="firstrun-scene">
           <div className="fxaccounts-container">
             <div className="firstrun-left-divider">
-              <h1 className="firstrun-title"><FormattedMessage id="firstrun_title" /></h1>
-              <p className="firstrun-content"><FormattedMessage id="firstrun_content" /></p>
-              <a className="firstrun-link" href={`https://www.mozilla.org/firefox/features/sync/?${this.utmParams}`} target="_blank" rel="noopener noreferrer"><FormattedMessage id="firstrun_learn_more_link" /></a>
+              <h1 className="firstrun-title" data-l10n-id="onboarding-sync-welcome-header" />
+              <p className="firstrun-content" data-l10n-id="onboarding-sync-welcome-content" />
+              <a className="firstrun-link" href={`https://www.mozilla.org/firefox/features/sync/?${this.utmParams}`} target="_blank" rel="noopener noreferrer" data-l10n-id="onboarding-sync-welcome-learn-more-link" />
             </div>
             <div className="firstrun-sign-in">
-              <p className="form-header"><FormattedMessage id="firstrun_form_header" /><span className="sub-header"><FormattedMessage id="firstrun_form_sub_header" /></span></p>
+              <p className="form-header">
+                <span data-l10n-id="onboarding-sync-form-header"/>
+                <span className="sub-header" data-l10n-id="onboarding-sync-form-sub-header" />
+              </p>
               <form method="get" action={this.props.fxa_endpoint} target="_blank" rel="noopener noreferrer" onSubmit={this.onSubmit}>
                 <input name="service" type="hidden" value="sync" />
                 <input name="action" type="hidden" value="email" />
                 <input name="context" type="hidden" value="fx_desktop_v3" />
                 <input name="entrypoint" type="hidden" value="activity-stream-firstrun" />
                 <input name="utm_source" type="hidden" value="activity-stream" />
                 <input name="utm_campaign" type="hidden" value="firstrun" />
                 <input name="utm_medium" type="hidden" value="referral" />
                 <input name="utm_term" type="hidden" value="trailhead-control" />
                 <input name="device_id" type="hidden" value={this.state.deviceId} />
                 <input name="flow_id" type="hidden" value={this.state.flowId} />
                 <input name="flow_begin_time" type="hidden" value={this.state.flowBeginTime} />
-                <span className="error">{this.props.intl.formatMessage({id: "firstrun_invalid_input"})}</span>
-                <input className="email-input" name="email" type="email" required="true" onInvalid={this.onInputInvalid} placeholder={this.props.intl.formatMessage({id: "firstrun_email_input_placeholder"})} onChange={this.onInputChange} />
+                <span className="error" data-l10n-id="onboarding-sync-form-invalid-input" />
+                <input className="email-input" name="email" type="email" required="true" onInvalid={this.onInputInvalid} onChange={this.onInputChange} data-l10n-id="onboarding-sync-form-input" />
                 <div className="extra-links">
-                  <FormattedMessage
-                    id="firstrun_extra_legal_links"
-                    values={{
-                      terms: termsLink,
-                      privacy: privacyLink,
-                    }} />
+                <p data-l10n-id="onboarding-sync-legal-notice">
+                  <a data-l10n-name="terms" target="_blank" rel="noopener noreferrer"
+                    href={`${this.props.fxa_endpoint}/legal/terms?${this.utmParams}`} />
+                  <a data-l10n-name="privacy" target="_blank" rel="noopener noreferrer"
+                    href={`${this.props.fxa_endpoint}/legal/privacy?${this.utmParams}`} />
+                </p>
                 </div>
-                <button className="continue-button" type="submit"><FormattedMessage id="firstrun_continue_to_login" /></button>
+                <button className="continue-button" type="submit" data-l10n-id="onboarding-sync-form-continue-button" />
               </form>
-              <button className="skip-button" disabled={!!this.state.emailInput} onClick={this.clickSkip}><FormattedMessage id="firstrun_skip_login" /></button>
+              <button className="skip-button" disabled={!!this.state.emailInput} onClick={this.clickSkip} data-l10n-id="onboarding-sync-form-skip-login-button" />
             </div>
           </div>
         </div>
       </div>
     );
   }
 }
 
 const getState = state => ({fxa_endpoint: state.Prefs.values.fxa_endpoint});
-export const StartupOverlay = connect(getState)(injectIntl(_StartupOverlay));
+export const StartupOverlay = connect(getState)(_StartupOverlay);
--- a/browser/components/newtab/content-src/asrouter/templates/Trailhead/Trailhead.jsx
+++ b/browser/components/newtab/content-src/asrouter/templates/Trailhead/Trailhead.jsx
@@ -1,10 +1,9 @@
 import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
-import {injectIntl} from "react-intl";
 import {ModalOverlayWrapper} from "../../components/ModalOverlay/ModalOverlay";
 import {OnboardingCard} from "../OnboardingMessage/OnboardingMessage";
 import React from "react";
 
 const FLUENT_FILES = [
   "branding/brand.ftl",
   "browser/branding/brandings.ftl",
   "browser/branding/sync-brand.ftl",
@@ -17,17 +16,17 @@ const FOCUSABLE_SELECTOR = [
   "button:not([disabled]):not([tabindex='-1'])",
   "iframe:not([tabindex='-1'])",
   "input:not([disabled]):not([tabindex='-1'])",
   "select:not([disabled]):not([tabindex='-1'])",
   "textarea:not([disabled]):not([tabindex='-1'])",
   "[tabindex]:not([tabindex='-1'])",
 ].join(", ");
 
-export class _Trailhead extends React.PureComponent {
+export class Trailhead extends React.PureComponent {
   constructor(props) {
     super(props);
     this.closeModal = this.closeModal.bind(this);
     this.hideCardPanel = this.hideCardPanel.bind(this);
     this.onInputChange = this.onInputChange.bind(this);
     this.onStartBlur = this.onStartBlur.bind(this);
     this.onSubmit = this.onSubmit.bind(this);
     this.onInputInvalid = this.onInputInvalid.bind(this);
@@ -177,23 +176,16 @@ export class _Trailhead extends React.Pu
     this.setState({showCardPanel: false});
     this.props.onDismissBundle();
   }
 
   revealCards() {
     this.setState({showCards: true});
   }
 
-  getStringValue(str) {
-    if (str.property_id) {
-      str.value = this.props.intl.formatMessage({id: str.property_id});
-    }
-    return str.value;
-  }
-
   /**
    * Takes in a url as a string or URL object and returns a URL object with the
    * utm_* parameters added to it. If a URL object is passed in, the paraemeters
    * are added to it (the return value can be ignored in that case as it's the
    * same object).
    */
   addUtmParams(url, isCard = false) {
     let returnUrl = url;
@@ -228,77 +220,73 @@ export class _Trailhead extends React.Pu
 
   render() {
     const {props} = this;
     const {bundle: cards, content, utm_term} = props.message;
     const innerClassName = [
       "trailhead",
       content && content.className,
     ].filter(v => v).join(" ");
+
     return (<>
     {this.state.isModalOpen && content ? <ModalOverlayWrapper innerClassName={innerClassName} onClose={this.closeModal} id="trailheadDialog" headerId="trailheadHeader">
       <div className="trailheadInner">
         <div className="trailheadContent">
           <h1 data-l10n-id={content.title.string_id}
-            id="trailheadHeader">{this.getStringValue(content.title)}</h1>
+            id="trailheadHeader" />
           {content.subtitle &&
-            <p data-l10n-id={content.subtitle.string_id}>{this.getStringValue(content.subtitle)}</p>
+            <p data-l10n-id={content.subtitle.string_id} />
           }
           <ul className="trailheadBenefits">
             {content.benefits.map(item => (
               <li key={item.id} className={item.id}>
-                <h3 data-l10n-id={item.title.string_id}>{this.getStringValue(item.title)}</h3>
-                <p data-l10n-id={item.text.string_id}>{this.getStringValue(item.text)}</p>
+                <h3 data-l10n-id={item.title.string_id} />
+                <p data-l10n-id={item.text.string_id} />
               </li>
             ))}
           </ul>
-          <a className="trailheadLearn" data-l10n-id={content.learn.text.string_id} href={this.addUtmParams(content.learn.url)} target="_blank" rel="noopener noreferrer">
-            {this.getStringValue(content.learn.text)}
-          </a>
+          <a className="trailheadLearn" data-l10n-id={content.learn.text.string_id} href={this.addUtmParams(content.learn.url)} target="_blank" rel="noopener noreferrer" />
         </div>
         <div role="group" aria-labelledby="joinFormHeader" aria-describedby="joinFormBody" className="trailheadForm">
-          <h3 id="joinFormHeader" data-l10n-id={content.form.title.string_id}>{this.getStringValue(content.form.title)}</h3>
-          <p id="joinFormBody" data-l10n-id={content.form.text.string_id}>{this.getStringValue(content.form.text)}</p>
+          <h3 id="joinFormHeader" data-l10n-id={content.form.title.string_id} />
+          <p id="joinFormBody" data-l10n-id={content.form.text.string_id} />
           <form method="get" action={this.props.fxaEndpoint} target="_blank" rel="noopener noreferrer" onSubmit={this.onSubmit}>
             <input name="service" type="hidden" value="sync" />
             <input name="action" type="hidden" value="email" />
             <input name="context" type="hidden" value="fx_desktop_v3" />
             <input name="entrypoint" type="hidden" value="activity-stream-firstrun" />
             <input name="utm_source" type="hidden" value="activity-stream" />
             <input name="utm_campaign" type="hidden" value="firstrun" />
             <input name="utm_term" type="hidden" value={utm_term} />
             <input name="device_id" type="hidden" value={this.state.deviceId} />
             <input name="flow_id" type="hidden" value={this.state.flowId} />
             <input name="flow_begin_time" type="hidden" value={this.state.flowBeginTime} />
             <input name="style" type="hidden" value="trailhead" />
             <p data-l10n-id="onboarding-join-form-email-error" className="error" />
             <input
               data-l10n-id={content.form.email.string_id}
-              placeholder={this.getStringValue(content.form.email)}
               name="email"
               type="email"
               onInvalid={this.onInputInvalid}
               onChange={this.onInputChange} />
             <p className="trailheadTerms" data-l10n-id="onboarding-join-form-legal">
               <a data-l10n-name="terms" target="_blank" rel="noopener noreferrer"
                 href={this.addUtmParams("https://accounts.firefox.com/legal/terms")} />
               <a data-l10n-name="privacy" target="_blank" rel="noopener noreferrer"
                 href={this.addUtmParams("https://accounts.firefox.com/legal/privacy")} />
             </p>
-            <button data-l10n-id={content.form.button.string_id} type="submit">
-              {this.getStringValue(content.form.button)}
-            </button>
+            <button data-l10n-id={content.form.button.string_id} type="submit" />
           </form>
         </div>
       </div>
 
       <button className="trailheadStart"
         data-l10n-id={content.skipButton.string_id}
         onBlur={this.onStartBlur}
-        onClick={this.closeModal}>{this.getStringValue(content.skipButton)}</button>
+        onClick={this.closeModal} />
     </ModalOverlayWrapper> : null}
     {(cards && cards.length) ? <div className={`trailheadCards ${this.state.showCardPanel ? "expanded" : "collapsed"}`}>
       <div className="trailheadCardsInner"
         aria-hidden={!this.state.showCards}>
         <h1 data-l10n-id="onboarding-welcome-header" />
         <div className={`trailheadCardGrid${this.state.showCards ? " show" : ""}`}>
         {cards.map(card => (
           <OnboardingCard key={card.id}
@@ -307,18 +295,15 @@ export class _Trailhead extends React.Pu
             onAction={this.onCardAction}
             UISurface="TRAILHEAD"
             {...card} />
         ))}
         </div>
         {this.state.showCardPanel &&
           <button
             className="icon icon-dismiss" onClick={this.hideCardPanel}
-            title={props.intl.formatMessage({id: "menu_action_dismiss"})}
-            aria-label={props.intl.formatMessage({id: "menu_action_dismiss"})} />
+            data-l10n-id="onboarding-cards-dismiss" />
         }
       </div>
     </div> : null}
     </>);
   }
 }
-
-export const Trailhead = injectIntl(_Trailhead);
--- a/browser/components/newtab/content-src/components/Base/Base.jsx
+++ b/browser/components/newtab/content-src/components/Base/Base.jsx
@@ -1,51 +1,41 @@
 import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
-import {addLocaleData, IntlProvider} from "react-intl";
 import {ASRouterAdmin} from "content-src/components/ASRouterAdmin/ASRouterAdmin";
 import {ASRouterUISurface} from "../../asrouter/asrouter-content";
 import {ConfirmDialog} from "content-src/components/ConfirmDialog/ConfirmDialog";
 import {connect} from "react-redux";
 import {DiscoveryStreamBase} from "content-src/components/DiscoveryStreamBase/DiscoveryStreamBase";
 import {ErrorBoundary} from "content-src/components/ErrorBoundary/ErrorBoundary";
 import React from "react";
 import {Search} from "content-src/components/Search/Search";
 import {Sections} from "content-src/components/Sections/Sections";
 
 const PrefsButton = props => (
   <div className="prefs-button">
     <button className="icon icon-settings" onClick={props.onClick} data-l10n-id="newtab-settings-button" />
   </div>
 );
 
-// Add the locale data for pluralization and relative-time formatting for now,
-// this just uses english locale data. We can make this more sophisticated if
-// more features are needed.
-function addLocaleDataForReactIntl(locale) {
-  addLocaleData([{locale, parentLocale: "en"}]);
-}
-
 // Returns a function will not be continuously triggered when called. The
 // function will be triggered if called again after `wait` milliseconds.
 function debounce(func, wait) {
   let timer;
   return (...args) => {
     if (timer) { return; }
 
     let wakeUp = () => { timer = null; };
 
     timer = setTimeout(wakeUp, wait);
     func.apply(this, args);
   };
 }
 
 export class _Base extends React.PureComponent {
   componentWillMount() {
-    const {locale} = this.props;
-    addLocaleDataForReactIntl(locale);
     if (this.props.isFirstrun) {
       global.document.body.classList.add("welcome", "hide-main");
     }
   }
 
   componentWillUnmount() {
     this.updateTheme();
   }
@@ -63,31 +53,31 @@ export class _Base extends React.PureCom
       document.body.classList.contains("hide-main") ? "hide-main" : "",
       document.body.classList.contains("inline-onboarding") ? "inline-onboarding" : "",
     ].filter(v => v).join(" ");
     global.document.body.className = bodyClassName;
   }
 
   render() {
     const {props} = this;
-    const {App, locale, strings} = props;
+    const {App} = props;
     const isDevtoolsEnabled = props.Prefs.values["asrouter.devtoolsEnabled"];
 
     if (!App.initialized) {
       return null;
     }
 
-    return (<IntlProvider locale={locale} messages={strings}>
+    return (
       <ErrorBoundary className="base-content-fallback">
         <React.Fragment>
           <BaseContent {...this.props} />
           {isDevtoolsEnabled ? <ASRouterAdmin /> : null}
         </React.Fragment>
       </ErrorBoundary>
-    </IntlProvider>);
+    );
   }
 }
 
 export class BaseContent extends React.PureComponent {
   constructor(props) {
     super(props);
     this.openPreferences = this.openPreferences.bind(this);
     this.onWindowScroll = debounce(this.onWindowScroll.bind(this), 5);
--- a/browser/components/newtab/content-src/components/Card/Card.jsx
+++ b/browser/components/newtab/content-src/components/Card/Card.jsx
@@ -1,12 +1,11 @@
 import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
 import {cardContextTypes} from "./types";
 import {connect} from "react-redux";
-import {injectIntl} from "react-intl";
 import {LinkMenu} from "content-src/components/LinkMenu/LinkMenu";
 import React from "react";
 import {ScreenshotUtils} from "content-src/lib/screenshot-utils";
 
 // Keep track of pending image loads to only request once
 const gImageLoading = new Map();
 
 /**
@@ -242,17 +241,17 @@ export class _Card extends React.PureCom
               {fluentID && !link.context && <div className="card-context-label" data-l10n-id={fluentID} />}
               {link.context && <div className="card-context-label">{link.context}</div>}
             </div>
           </div>
         </div>
       </a>
       {!props.placeholder && <button aria-haspopup="true"
         data-l10n-id="newtab-menu-content-tooltip"
-        data-l10n-args={`{ "title": "${title}" }`}
+        data-l10n-args={JSON.stringify({title})}
         className="context-menu-button icon"
         onClick={this.onMenuButtonClick} />}
       {isContextMenuOpen &&
         <LinkMenu
           dispatch={dispatch}
           index={index}
           source={eventSource}
           onUpdate={this.onMenuUpdate}
@@ -260,10 +259,10 @@ export class _Card extends React.PureCom
           site={link}
           siteInfo={this._getTelemetryInfo()}
           shouldSendImpressionStats={shouldSendImpressionStats} />
       }
    </li>);
   }
 }
 _Card.defaultProps = {link: {}};
-export const Card = connect(state => ({platform: state.Prefs.values.platform}))(injectIntl(_Card));
+export const Card = connect(state => ({platform: state.Prefs.values.platform}))(_Card);
 export const PlaceholderCard = props => <Card placeholder={true} className={props.className} />;
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx
@@ -62,17 +62,16 @@ export class DSCard extends React.PureCo
             }]}
             dispatch={this.props.dispatch}
             source={this.props.type} />
         </SafeAnchor>
         {!this.props.placeholder && <DSLinkMenu
           id={this.props.id}
           index={this.props.pos}
           dispatch={this.props.dispatch}
-          intl={this.props.intl}
           url={this.props.url}
           title={this.props.title}
           source={this.props.source}
           type={this.props.type}
           pocket_id={this.props.pocket_id}
           shim={this.props.shim}
           bookmarkGuid={this.props.bookmarkGuid} />}
       </div>
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx
@@ -37,17 +37,23 @@ export class DSImage extends React.PureC
   reformatImageURL(url, width, height) {
     // Change the image URL to request a size tailored for the parent container width
     // Also: force JPEG, quality 60, no upscaling, no EXIF data
     // Uses Thumbor: https://thumbor.readthedocs.io/en/latest/usage.html
     return `https://img-getpocket.cdn.mozilla.net/${width}x${height}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/${encodeURIComponent(url)}`;
   }
 
   componentDidMount() {
-    this.observer = new IntersectionObserver(this.onSeen.bind(this));
+    this.observer = new IntersectionObserver(this.onSeen.bind(this), {
+      // Assume an image will be eventually seen if it is within 520px of the viewport
+      // This is half the average Desktop vertical screen size:
+      // http://gs.statcounter.com/screen-resolution-stats/desktop/north-america
+      rootMargin: `540px`,
+    });
+
     this.observer.observe(ReactDOM.findDOMNode(this));
   }
 
   componentWillUnmount() {
     // Remove observer on unmount
     if (this.observer) {
       this.observer.unobserve(ReactDOM.findDOMNode(this));
     }
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx
@@ -1,13 +1,12 @@
-import {injectIntl} from "react-intl";
 import {LinkMenu} from "content-src/components/LinkMenu/LinkMenu";
 import React from "react";
 
-export class _DSLinkMenu extends React.PureComponent {
+export class DSLinkMenu extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
       activeCard: null,
       showContextMenu: false,
     };
     this.windowObj = this.props.windowObj || window; // Added to support unit tests
     this.onMenuButtonClick = this.onMenuButtonClick.bind(this);
@@ -53,17 +52,17 @@ export class _DSLinkMenu extends React.P
     const type = this.props.type || "DISCOVERY_STREAM";
     const title = this.props.title || this.props.source;
 
     return (<div>
       <button ref={this.contextMenuButtonRef}
               aria-haspopup="true"
               className="context-menu-button icon"
               data-l10n-id="newtab-menu-content-tooltip"
-              data-l10n-args={`{ "title": "${title}" }`}
+              data-l10n-args={JSON.stringify({title})}
               onClick={this.onMenuButtonClick} />
       {isContextMenuOpen &&
         <LinkMenu
           dispatch={dispatch}
           index={index}
           source={type.toUpperCase()}
           onUpdate={this.onMenuUpdate}
           onShow={this.onMenuShow}
@@ -78,10 +77,8 @@ export class _DSLinkMenu extends React.P
             pocket_id: this.props.pocket_id,
             shim: this.props.shim,
             bookmarkGuid: this.props.bookmarkGuid,
           }} />
       }
     </div>);
   }
 }
-
-export const DSLinkMenu = injectIntl(_DSLinkMenu);
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx
@@ -100,17 +100,16 @@ export class Hero extends React.PureComp
               }]}
               dispatch={this.props.dispatch}
               source={this.props.type} />
           </SafeAnchor>
           <DSLinkMenu
             id={heroRec.id}
             index={heroRec.pos}
             dispatch={this.props.dispatch}
-            intl={this.props.intl}
             url={heroRec.url}
             title={heroRec.title}
             source={heroRec.domain}
             type={this.props.type}
             pocket_id={heroRec.pocket_id}
             shim={heroRec.shim}
             bookmarkGuid={heroRec.bookmarkGuid} />
         </div>
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/List/List.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/List/List.jsx
@@ -71,17 +71,16 @@ export class ListItem extends React.Pure
             }]}
             dispatch={this.props.dispatch}
             source={this.props.type} />
         </SafeAnchor>