Bug 1385133 - Add Snippets v5 support, responsive layout and bug fixes to Activity Stream. r?dmose draft
authorEd Lee <edilee@mozilla.com>
Fri, 28 Jul 2017 17:29:39 -0700
changeset 617925 90039fd8de4c502c5693409a8f5f1f802a386d59
parent 617496 16ffc1d05422a81099ce8b9b59de66dde4c8b2f0
child 639914 e640c85b2a11f542589de171986822b27de643af
push id71162
push userbmo:edilee@mozilla.com
push dateSat, 29 Jul 2017 00:29:55 +0000
reviewersdmose
bugs1385133
milestone56.0a1
Bug 1385133 - Add Snippets v5 support, responsive layout and bug fixes to Activity Stream. r?dmose MozReview-Commit-ID: AnJvI2kMcnE
browser/extensions/activity-stream/data/content/activity-stream.bundle.js
browser/extensions/activity-stream/data/content/activity-stream.css
browser/extensions/activity-stream/data/content/activity-stream.html
browser/extensions/activity-stream/data/locales.json
browser/extensions/activity-stream/lib/ActivityStream.jsm
browser/extensions/activity-stream/lib/ManualMigration.jsm
browser/extensions/activity-stream/lib/SnippetsFeed.jsm
browser/extensions/activity-stream/lib/TopStoriesFeed.jsm
browser/extensions/activity-stream/test/unit/lib/ManualMigration.test.js
browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js
browser/extensions/activity-stream/test/unit/lib/TopStoriesFeed.test.js
--- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -811,26 +811,48 @@ class SnippetsMap extends Map {
     this._db = null;
   }
 
   set(key, value) {
     super.set(key, value);
     return this._dbTransaction(db => db.put(value, key));
   }
 
-  delete(key, value) {
+  delete(key) {
     super.delete(key);
     return this._dbTransaction(db => db.delete(key));
   }
 
   clear() {
     super.clear();
     return this._dbTransaction(db => db.clear());
   }
 
+  get blockList() {
+    return this.get("blockList") || [];
+  }
+
+  /**
+   * blockSnippetById - Blocks a snippet given an id
+   *
+   * @param  {str|int} id   The id of the snippet
+   * @return {Promise}      Resolves when the id has been written to indexedDB,
+   *                        or immediately if the snippetMap is not connected
+   */
+  async blockSnippetById(id) {
+    if (!id) {
+      return;
+    }
+    let blockList = this.blockList;
+    if (!blockList.includes(id)) {
+      blockList.push(id);
+    }
+    await this.set("blockList", blockList);
+  }
+
   /**
    * connect - Attaches an indexedDB back-end to the Map so that any set values
    *           are also cached in a store. It also restores any existing values
    *           that are already stored in the indexedDB store.
    *
    * @return {type}  description
    */
   async connect() {
@@ -975,17 +997,16 @@ class SnippetsProvider {
   }
 
   _showDefaultSnippets() {
     // TODO
   }
 
   _showRemoteSnippets() {
     const snippetsEl = document.getElementById(this.elementId);
-    const containerEl = document.getElementById(this.containerElementId);
     const payload = this.snippetsMap.get("snippets");
 
     if (!snippetsEl) {
       throw new Error(`No element was found with id '${this.elementId}'.`);
     }
 
     // This could happen if fetching failed
     if (!payload) {
@@ -997,38 +1018,31 @@ class SnippetsProvider {
 
     // Scripts injected by innerHTML are inactive, so we have to relocate them
     // through DOM manipulation to activate their contents.
     for (const scriptEl of snippetsEl.getElementsByTagName("script")) {
       const relocatedScript = document.createElement("script");
       relocatedScript.text = scriptEl.text;
       scriptEl.parentNode.replaceChild(relocatedScript, scriptEl);
     }
-
-    // Unhide the container if everything went OK
-    if (containerEl) {
-      containerEl.style.display = "block";
-    }
   }
 
   /**
    * init - Fetch the snippet payload and show snippets
    *
    * @param  {obj} options
    * @param  {str} options.appData.snippetsURL  The URL from which we fetch snippets
    * @param  {int} options.appData.version  The current snippets version
    * @param  {str} options.elementId  The id of the element in which to inject snippets
-   * @param  {str} options.containerElementId  The id of the element of element containing the snippets element
    * @param  {bool} options.connect  Should gSnippetsMap connect to indexedDB?
    */
   async init(options) {
     Object.assign(this, {
       appData: {},
       elementId: "snippets",
-      containerElementId: "snippets-container",
       connect: true
     }, options);
 
     // TODO: Requires enabling indexedDB on newtab
     // Restore the snippets map from indexedDB
     if (this.connect) {
       try {
         await this.snippetsMap.connect();
@@ -1049,19 +1063,45 @@ class SnippetsProvider {
     try {
       this._showRemoteSnippets();
     } catch (e) {
       this._showDefaultSnippets(e);
     }
   }
 }
 
-module.exports.SnippetsMap = SnippetsMap;
-module.exports.SnippetsProvider = SnippetsProvider;
-module.exports.SNIPPETS_UPDATE_INTERVAL_MS = SNIPPETS_UPDATE_INTERVAL_MS;
+/**
+ * addSnippetsSubscriber - Creates a SnippetsProvider that Initializes
+ *                         when the store has received the appropriate
+ *                         Snippet data.
+ *
+ * @param  {obj} store   The redux store
+ * @return {obj}        Returns the snippets instance and unsubscribe function
+ */
+function addSnippetsSubscriber(store) {
+  const snippets = new SnippetsProvider();
+  const unsubscribe = store.subscribe(() => {
+    const state = store.getState();
+    if (state.Snippets.initialized) {
+      if (state.Snippets.onboardingFinished) {
+        snippets.init({ appData: state.Snippets });
+      }
+      unsubscribe();
+    }
+  });
+  // These values are returned for testing purposes
+  return snippets;
+}
+
+module.exports = {
+  addSnippetsSubscriber,
+  SnippetsMap,
+  SnippetsProvider,
+  SNIPPETS_UPDATE_INTERVAL_MS
+};
 /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(24)))
 
 /***/ }),
 /* 11 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 /* This Source Code Form is subject to the terms of the Mozilla Public
@@ -1425,17 +1465,18 @@ class Card extends React.Component {
   onMenuUpdate(showContextMenu) {
     this.setState({ showContextMenu });
   }
   render() {
     var _props = this.props;
     const index = _props.index,
           link = _props.link,
           dispatch = _props.dispatch,
-          contextMenuOptions = _props.contextMenuOptions;
+          contextMenuOptions = _props.contextMenuOptions,
+          eventSource = _props.eventSource;
 
     const isContextMenuOpen = this.state.showContextMenu && this.state.activeCard === index;
     const hostname = shortURL(link);
     var _cardContextTypes$lin = cardContextTypes[link.type];
     const icon = _cardContextTypes$lin.icon,
           intlID = _cardContextTypes$lin.intlID;
 
 
@@ -1498,16 +1539,17 @@ class Card extends React.Component {
           "span",
           { className: "sr-only" },
           `Open context menu for ${link.title}`
         )
       ),
       React.createElement(LinkMenu, {
         dispatch: dispatch,
         index: index,
+        source: eventSource,
         onUpdate: this.onMenuUpdate,
         options: link.context_menu_options || contextMenuOptions,
         site: link,
         visible: isContextMenuOpen })
     );
   }
 }
 module.exports = Card;
@@ -1937,17 +1979,17 @@ class PreferencesPane extends React.Comp
               "p",
               null,
               React.createElement(FormattedMessage, { id: "settings_pane_body" })
             ),
             React.createElement(PreferencesInput, { className: "showSearch", prefName: "showSearch", value: prefs.showSearch, onChange: this.handleChange,
               titleStringId: "settings_pane_search_header", descStringId: "settings_pane_search_body" }),
             React.createElement(PreferencesInput, { className: "showTopSites", prefName: "showTopSites", value: prefs.showTopSites, onChange: this.handleChange,
               titleStringId: "settings_pane_topsites_header", descStringId: "settings_pane_topsites_body" }),
-            this.topStoriesOptions && React.createElement(PreferencesInput, { className: "showTopStories", prefName: "feeds.section.topstories",
+            this.topStoriesOptions && !this.topStoriesOptions.hidden && React.createElement(PreferencesInput, { className: "showTopStories", prefName: "feeds.section.topstories",
               value: prefs["feeds.section.topstories"], onChange: this.handleChange,
               titleStringId: "header_recommended_by", titleStringValues: { provider: this.topStoriesOptions.provider_name },
               descStringId: this.topStoriesOptions.provider_description })
           ),
           React.createElement(
             "section",
             { className: "actions" },
             React.createElement(
@@ -2650,33 +2692,25 @@ const initStore = __webpack_require__(9)
 var _require2 = __webpack_require__(11);
 
 const reducers = _require2.reducers;
 
 const DetectUserSessionStart = __webpack_require__(8);
 
 var _require3 = __webpack_require__(10);
 
-const SnippetsProvider = _require3.SnippetsProvider;
+const addSnippetsSubscriber = _require3.addSnippetsSubscriber;
 
 
 new DetectUserSessionStart().sendEventOrAddListener();
 
 const store = initStore(reducers);
 
 ReactDOM.render(React.createElement(
   Provider,
   { store: store },
   React.createElement(Base, null)
 ), document.getElementById("root"));
 
-// Trigger snippets when snippets data has been received.
-const snippets = new SnippetsProvider();
-const unsubscribe = store.subscribe(() => {
-  const state = store.getState();
-  if (state.Snippets.initialized) {
-    snippets.init({ appData: state.Snippets });
-    unsubscribe();
-  }
-});
+addSnippetsSubscriber(store);
 
 /***/ })
 /******/ ]);
\ No newline at end of file
--- a/browser/extensions/activity-stream/data/content/activity-stream.css
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -144,40 +144,31 @@ a {
       box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1);
       transition: box-shadow 150ms; }
     .actions button.done {
       background: #0695F9;
       border: solid 1px #1677CF;
       color: #FFF;
       margin-inline-start: auto; }
 
-#snippets-container {
-  display: none;
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  background: white;
-  height: 122px; }
-
-#snippets {
-  max-width: 736px;
-  margin: 0 auto; }
-
 .outer-wrapper {
   display: flex;
   flex-grow: 1;
   padding: 62px 32px 32px;
   height: 100%; }
 
 main {
-  margin: auto; }
-  @media (min-width: 672px) {
+  margin: auto;
+  width: 224px; }
+  @media (min-width: 416px) {
     main {
-      width: 608px; } }
+      width: 352px; } }
+  @media (min-width: 544px) {
+    main {
+      width: 480px; } }
   @media (min-width: 800px) {
     main {
       width: 736px; } }
   main section {
     margin-bottom: 40px; }
 
 .section-title {
   color: #6E707E;
@@ -188,19 +179,19 @@ main {
   .section-title span {
     vertical-align: middle; }
 
 .top-sites-list {
   list-style: none;
   margin: 0;
   padding: 0;
   margin-inline-end: -32px; }
-  @media (min-width: 672px) {
+  @media (min-width: 544px) {
     .top-sites-list {
-      width: 640px; } }
+      width: 512px; } }
   @media (min-width: 800px) {
     .top-sites-list {
       width: 768px; } }
   .top-sites-list li {
     display: inline-block;
     margin: 0 0 18px;
     margin-inline-end: 32px; }
   .top-sites-list .top-site-outer {
@@ -336,19 +327,21 @@ main {
     margin: 0;
     margin-top: 12px; }
   .sections-list .section-top-bar .info-option-link {
     display: block;
     margin-top: 12px;
     color: #0A84FF; }
 
 .sections-list .section-list {
-  width: 768px;
   clear: both;
-  margin: 0; }
+  margin: 0;
+  display: grid;
+  grid-template-columns: repeat(auto-fit, 224px);
+  grid-gap: 32px; }
 
 .sections-list .section-empty-state {
   width: 100%;
   height: 266px;
   display: flex;
   border: solid 1px rgba(0, 0, 0, 0.1);
   border-radius: 3px;
   margin-bottom: 16px; }
@@ -371,17 +364,18 @@ main {
       font-weight: 300;
       color: #A0A0A0;
       text-align: center; }
 
 .topic {
   font-size: 13px;
   color: #BFC0C7;
   min-width: 780px;
-  line-height: 16px; }
+  line-height: 16px;
+  margin-top: 16px; }
   .topic ul {
     display: inline;
     padding-left: 12px; }
   .topic ul li {
     display: inline; }
   .topic ul li::after {
     content: '•';
     padding-left: 8px;
@@ -659,17 +653,16 @@ main {
   border-radius: 3px;
   font-size: 14px;
   z-index: 11002; }
 
 .card-outer {
   background: #FFF;
   display: inline-block;
   margin-inline-end: 32px;
-  margin-bottom: 16px;
   width: 224px;
   border-radius: 3px;
   border-color: rgba(0, 0, 0, 0.1);
   height: 266px;
   position: relative; }
   .card-outer .context-menu-button {
     cursor: pointer;
     position: absolute;
@@ -764,35 +757,58 @@ main {
     flex-grow: 1;
     overflow: hidden;
     text-overflow: ellipsis;
     white-space: nowrap; }
 
 .manual-migration-container {
   background: rgba(215, 215, 219, 0.5);
   font-size: 13px;
-  height: 50px;
   border-radius: 2px;
   margin-bottom: 40px;
-  display: flex;
-  justify-content: space-between; }
+  padding: 20px;
+  text-align: center; }
+  @media (min-width: 544px) {
+    .manual-migration-container {
+      display: flex;
+      justify-content: space-between;
+      height: 100px;
+      padding: 0;
+      text-align: left; } }
+  @media (min-width: 800px) {
+    .manual-migration-container {
+      height: 50px; } }
   .manual-migration-container p {
-    margin: 0 4px 0 12px;
-    align-self: center;
-    display: flex;
-    justify-content: space-between; }
+    margin: 0; }
+    @media (min-width: 544px) {
+      .manual-migration-container p {
+        margin-inline-end: 4px;
+        margin-inline-start: 12px;
+        align-self: center;
+        display: flex;
+        justify-content: space-between; } }
   .manual-migration-container .icon {
-    margin: 0 12px 0 0;
-    align-self: center; }
+    display: none; }
+    @media (min-width: 544px) {
+      .manual-migration-container .icon {
+        display: block;
+        margin: 0;
+        margin-inline-end: 12px;
+        align-self: center; } }
 
 .manual-migration-actions {
-  display: flex;
-  justify-content: space-between;
   border: none;
-  padding: 0; }
+  display: block;
+  padding: 10px 0 0 0;
+  line-height: 34px; }
+  @media (min-width: 544px) {
+    .manual-migration-actions {
+      display: flex;
+      justify-content: space-between;
+      padding: 0; } }
   .manual-migration-actions button {
     align-self: center;
     padding: 0 12px;
     height: 24px;
-    margin-inline-end: 0;
-    margin-right: 12px;
+    margin-inline-end: 12px;
     font-size: 13px;
-    width: 106px; }
+    min-width: 100px;
+    white-space: nowrap; }
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -4,17 +4,16 @@
     <meta charset="utf-8">
     <title></title>
     <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
   </head>
   <body class="activity-stream">
     <div id="root"></div>
     <div id="snippets-container">
-      <div id="topSection"></div> <!-- TODO: placeholder for v4 snippets. It should be removed when we switch to v5 -->
       <div id="snippets"></div>
     </div>
     <script src="chrome://browser/content/contentSearchUI.js"></script>
     <script src="resource://activity-stream/vendor/react.js"></script>
     <script src="resource://activity-stream/vendor/react-dom.js"></script>
     <script src="resource://activity-stream/vendor/react-intl.js"></script>
     <script src="resource://activity-stream/vendor/redux.js"></script>
     <script src="resource://activity-stream/vendor/react-redux.js"></script>
--- a/browser/extensions/activity-stream/data/locales.json
+++ b/browser/extensions/activity-stream/data/locales.json
@@ -231,24 +231,26 @@
   },
   "be": {
     "newtab_page_title": "Новая картка",
     "default_label_loading": "Загрузка…",
     "header_top_sites": "Папулярныя сайты",
     "header_stories": "Галоўныя навіны",
     "header_visit_again": "Наведаць зноў",
     "header_bookmarks": "Нядаўнія закладкі",
+    "header_recommended_by": "Рэкамендавана {provider}",
     "header_bookmarks_placeholder": "У вас яшчэ няма закладак.",
     "header_stories_from": "ад",
     "type_label_visited": "Наведанае",
     "type_label_bookmarked": "У закладках",
     "type_label_synced": "Сінхранізаванае з іншай прылады",
     "type_label_recommended": "Тэндэнцыі",
     "type_label_open": "Адкрыта",
     "type_label_topic": "Тэма",
+    "type_label_now": "Зараз",
     "menu_action_bookmark": "У закладкі",
     "menu_action_remove_bookmark": "Выдаліць закладку",
     "menu_action_copy_address": "Скапіраваць адрас",
     "menu_action_email_link": "Даслаць спасылку…",
     "menu_action_open_new_window": "Адкрыць у новым акне",
     "menu_action_open_private_window": "Адкрыць у новым прыватным акне",
     "menu_action_dismiss": "Адхіліць",
     "menu_action_delete": "Выдаліць з гісторыі",
@@ -257,16 +259,17 @@
     "confirm_history_delete_p1": "Вы сапраўды жадаеце выдаліць усе запісы аб гэтай старонцы з гісторыі?",
     "confirm_history_delete_notice_p2": "Гэта дзеянне немагчыма адмяніць.",
     "menu_action_save_to_pocket": "Захаваць у Pocket",
     "search_for_something_with": "Шукаць {search_term} у:",
     "search_button": "Шукаць",
     "search_header": "Шукаць у {search_engine_name}",
     "search_web_placeholder": "Пошук у Інтэрнэце",
     "search_settings": "Змяніць налады пошуку",
+    "section_info_option": "Звесткі",
     "welcome_title": "Калі ласка ў новую картку",
     "welcome_body": "Firefox будзе выкарыстоўваць гэта месца, каб адлюстроўваць самыя актуальныя закладкі, артыкулы, відэа і старонкі, якія вы нядаўна наведалі, каб вы змаглі лёгка трапіць на іх зноў.",
     "welcome_label": "Вызначэнне вашага выбранага",
     "time_label_less_than_minute": "<1 хв",
     "time_label_minute": "{number} хв",
     "time_label_hour": "{number} г",
     "time_label_day": "{number} д",
     "settings_pane_button_label": "Наладзіць вашу старонку новай карткі",
@@ -301,33 +304,39 @@
     "topsites_form_add_button": "Дадаць",
     "topsites_form_save_button": "Захаваць",
     "topsites_form_cancel_button": "Скасаваць",
     "topsites_form_url_validation": "Патрабуецца сапраўдны URL",
     "pocket_read_more": "Папулярныя тэмы:",
     "pocket_read_even_more": "Іншыя навіны",
     "pocket_feedback_header": "Лепшае з Сеціва, што адабралі больш за 25 мільёнаў чалавек.",
     "pocket_feedback_body": "Pocket, частка сям'і Mozilla, дапаможа падключыць вас да якаснага зместу, які вы можаце не знайсці іншым чынам.",
-    "pocket_send_feedback": "Даслаць водгук"
+    "pocket_send_feedback": "Даслаць водгук",
+    "topstories_empty_state": "Гатова. Праверце пазней, каб убачыць больш матэрыялаў ад {provider}. Не жадаеце чакаць? Выберыце папулярную тэму, каб знайсці больш цікавых матэрыялаў з усяго Інтэрнэту.",
+    "manual_migration_explanation": "Паспрабуйце Firefox з вашымі любімымі сайтамі і закладкамі з іншага браўзера.",
+    "manual_migration_cancel_button": "Не, дзякуй",
+    "manual_migration_import_button": "Імпартаваць зараз"
   },
   "bg": {
     "newtab_page_title": "Нов раздел",
     "default_label_loading": "Зареждане…",
     "header_top_sites": "Най-посещавани",
     "header_stories": "Популярни",
     "header_visit_again": "Посещаване",
     "header_bookmarks": "Последни отметки",
+    "header_recommended_by": "Препоръчано от {provider}",
     "header_bookmarks_placeholder": "Все още нямате отметки.",
     "header_stories_from": "от",
     "type_label_visited": "Посетена",
     "type_label_bookmarked": "Отметната",
     "type_label_synced": "Синхронизирана от друго устройство",
     "type_label_recommended": "Тенденции",
     "type_label_open": "Отваряне",
     "type_label_topic": "Тема",
+    "type_label_now": "Сега",
     "menu_action_bookmark": "Отметка",
     "menu_action_remove_bookmark": "Премахване на отметка",
     "menu_action_copy_address": "Копиране на адрес",
     "menu_action_email_link": "Препратка по ел. поща…",
     "menu_action_open_new_window": "Отваряне в раздел",
     "menu_action_open_private_window": "Отваряне в поверителен прозорец",
     "menu_action_dismiss": "Отхвърляне",
     "menu_action_delete": "Премахване от историята",
@@ -336,16 +345,17 @@
     "confirm_history_delete_p1": "Сигурни ли сте, че желаете да премахнете страницата навсякъде от историята?",
     "confirm_history_delete_notice_p2": "Действието е необратимо.",
     "menu_action_save_to_pocket": "Запазване в Pocket",
     "search_for_something_with": "Търсене на {search_term} с:",
     "search_button": "Търсене",
     "search_header": "Търсене с {search_engine_name}",
     "search_web_placeholder": "Търсене в интернет",
     "search_settings": "Настройки на търсене",
+    "section_info_option": "Информация",
     "welcome_title": "Добре дошли в нов раздел",
     "welcome_body": "Firefox ще използва това място, за да ви покаже най-подходящите отметки, статии, видео и страници, които сте посетили наскоро, така че да се върнете към тях лесно.",
     "welcome_label": "Търсене на акценти",
     "time_label_less_than_minute": "<1м",
     "time_label_minute": "{number} м",
     "time_label_hour": "{number} ч",
     "time_label_day": "{number} д",
     "settings_pane_button_label": "Настройки на новия раздел",
@@ -380,17 +390,21 @@
     "topsites_form_add_button": "Добавяне",
     "topsites_form_save_button": "Запазване",
     "topsites_form_cancel_button": "Отказ",
     "topsites_form_url_validation": "Необходим е валиден URL",
     "pocket_read_more": "Популярни теми:",
     "pocket_read_even_more": "Повече статии",
     "pocket_feedback_header": "Най-доброто от интернет, подбрано от над 25 милиона души.",
     "pocket_feedback_body": "Pocket, част от семейството на Mozilla, ще ви помогне да намерите висококачествено съдържанието, което може да не сте открили до сега.",
-    "pocket_send_feedback": "Обратна връзка"
+    "pocket_send_feedback": "Обратна връзка",
+    "topstories_empty_state": "Разгледахте всичко. Проверете по-късно за повече истории от {provider}. Нямате търпение? Изберете популярна тема, за да откриете повече истории из цялата Мрежа.",
+    "manual_migration_explanation": "Изпробвайте Firefox със своите любими сайтове и отметки от друг четец.",
+    "manual_migration_cancel_button": "Не, благодаря",
+    "manual_migration_import_button": "Внасяне"
   },
   "bn-BD": {
     "newtab_page_title": "নতুন ট্যাব",
     "default_label_loading": "লোড হচ্ছে…",
     "header_top_sites": "শীর্ঘ সাইট",
     "header_stories": "শীর্ষ গল্প",
     "header_visit_again": "পুনরায় ভিজিট করুন",
     "header_bookmarks": "সাম্প্রতিক বুকমার্ক",
@@ -1228,24 +1242,26 @@
   "en-ZA": {},
   "eo": {
     "newtab_page_title": "Nova legosigno",
     "default_label_loading": "Ŝargado…",
     "header_top_sites": "Plej vizititaj",
     "header_stories": "Ĉefaj artikoloj",
     "header_visit_again": "Viziti denove",
     "header_bookmarks": "Ĵusaj legosignoj",
+    "header_recommended_by": "Rekomendita de {provider}",
     "header_bookmarks_placeholder": "Vi ankoraŭ ne havas legosignojn.",
     "header_stories_from": "el",
     "type_label_visited": "Vizititaj",
     "type_label_bookmarked": "Kun legosigno",
     "type_label_synced": "Spegulitaj el alia aparato",
     "type_label_recommended": "Tendencoj",
     "type_label_open": "Malfermita",
     "type_label_topic": "Temo",
+    "type_label_now": "Nun",
     "menu_action_bookmark": "Aldoni legosignon",
     "menu_action_remove_bookmark": "Forigi legosignon",
     "menu_action_copy_address": "Kopii adreson",
     "menu_action_email_link": "Sendi ligilon retpoŝte…",
     "menu_action_open_new_window": "Malfermi en nova fenestro",
     "menu_action_open_private_window": "Malfermi en nova privata fenestro",
     "menu_action_dismiss": "Ignori",
     "menu_action_delete": "Forigi el historio",
@@ -1254,16 +1270,17 @@
     "confirm_history_delete_p1": "Ĉu vi certe volas forigi ĉiun aperon de tiu ĉi paĝo el via historio?",
     "confirm_history_delete_notice_p2": "Tiu ĉi ago ne estas malfarebla.",
     "menu_action_save_to_pocket": "Konservi en Pocket",
     "search_for_something_with": "Serĉi {search_term} per:",
     "search_button": "Serĉi",
     "search_header": "Serĉo de {search_engine_name}",
     "search_web_placeholder": "Serĉi la Teksaĵon",
     "search_settings": "Modifi serĉajn agordojn",
+    "section_info_option": "Informo",
     "welcome_title": "Bonvenon al nova langeto",
     "welcome_body": "Firefox uzos tiun ĉi spacon por montri al vi viaj plej gravajn legosignojn, artikolojn, filmetojn kaj paĝojn, kiujn vi vizitis antaŭ nelonge, tiel ke vi povos reiri al ili facile.",
     "welcome_label": "Elstaraĵoj identigataj",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
     "time_label_day": "{number}t",
     "settings_pane_button_label": "Personecigi la paĝon por novaj langetoj",
@@ -1298,17 +1315,21 @@
     "topsites_form_add_button": "Aldoni",
     "topsites_form_save_button": "Konservi",
     "topsites_form_cancel_button": "Nuligi",
     "topsites_form_url_validation": "Valida retadreso estas postulata",
     "pocket_read_more": "Ĉefaj temoj:",
     "pocket_read_even_more": "Montri pli da artikoloj",
     "pocket_feedback_header": "La plejbono el la Teksaĵo, reviziita de pli ol 25 milionoj da personoj.",
     "pocket_feedback_body": "Pocket, parto de la familio de Mozilla, helpos vin trovi altkvalitan enhavon, kiun vi eble ne trovos aliloke.",
-    "pocket_send_feedback": "Sendi komentojn"
+    "pocket_send_feedback": "Sendi komentojn",
+    "topstories_empty_state": "Vi legis ĉion. Kontrolu denove poste ĉu estas pli da novaĵon de {provider}. Ĉu vi ne povas atendi? Elektu popularan temon por trovi pli da interesaj artikoloj en la tuta teksaĵo.",
+    "manual_migration_explanation": "Provu Firefox kun viaj plej vizitataj retejoj kaj legosignoj el alia retumilo.",
+    "manual_migration_cancel_button": "Ne, dankon",
+    "manual_migration_import_button": "Importi nun"
   },
   "es-AR": {
     "newtab_page_title": "Nueva pestaña",
     "default_label_loading": "Cargando…",
     "header_top_sites": "Más visitados",
     "header_stories": "Historias principales",
     "header_visit_again": "Visitar de nuevo",
     "header_bookmarks": "Marcadores recientes",
@@ -1557,24 +1578,26 @@
   },
   "es-MX": {
     "newtab_page_title": "Nueva pestaña",
     "default_label_loading": "Cargando…",
     "header_top_sites": "Sitios favoritos",
     "header_stories": "Historias populares",
     "header_visit_again": "Visitar de nuevo",
     "header_bookmarks": "Marcadores recientes",
+    "header_recommended_by": "Recomendado por {provider}",
     "header_bookmarks_placeholder": "Aún no tienes ningún marcador.",
     "header_stories_from": "de",
     "type_label_visited": "Visitados",
     "type_label_bookmarked": "Marcados",
     "type_label_synced": "Sincronizado desde otro dispositivo",
     "type_label_recommended": "Tendencias",
     "type_label_open": "Abrir",
     "type_label_topic": "Tema",
+    "type_label_now": "Ahora",
     "menu_action_bookmark": "Marcador",
     "menu_action_remove_bookmark": "Eliminar marcador",
     "menu_action_copy_address": "Copiar dirección",
     "menu_action_email_link": "Enlace por correo electrónico…",
     "menu_action_open_new_window": "Abrir en una Nueva Ventana",
     "menu_action_open_private_window": "Abrir en una Nueva Ventana Privada",
     "menu_action_dismiss": "Descartar",
     "menu_action_delete": "Eliminar del historial",
@@ -1583,16 +1606,17 @@
     "confirm_history_delete_p1": "¿Estás seguro de que quieres eliminar de tu historial todas las instancias de esta página?",
     "confirm_history_delete_notice_p2": "Esta acción no se puede deshacer.",
     "menu_action_save_to_pocket": "Guardar en Pocket",
     "search_for_something_with": "Buscar {search_term} con:",
     "search_button": "Buscar",
     "search_header": "Buscar {search_engine_name}",
     "search_web_placeholder": "Buscar en la Web",
     "search_settings": "Cambiar configuraciones de búsqueda",
+    "section_info_option": "Información",
     "welcome_title": "Bienvenido a una nueva pestaña",
     "welcome_body": "Firefox usará este espacio para mostrar tus marcadores, artículos, videos y páginas más relevantes que se hayan visitado para poder volver más fácilmente.",
     "welcome_label": "Identificando tus destacados",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
     "time_label_day": "{number}d",
     "settings_pane_button_label": "Personalizar tu página de nueva pestaña",
@@ -1627,17 +1651,21 @@
     "topsites_form_add_button": "Agregar",
     "topsites_form_save_button": "Guardar",
     "topsites_form_cancel_button": "Cancelar",
     "topsites_form_url_validation": "Se requiere una URL válida",
     "pocket_read_more": "Temas populares:",
     "pocket_read_even_more": "Ver más historias",
     "pocket_feedback_header": "Lo mejor de la web, seleccionado por más 25 millones de personas.",
     "pocket_feedback_body": "Pocket, miembro de la familia Mozilla, te ayuda a conectarte con contenido de alta calidad que tal vez no hubieras encontrado de otra forma.",
-    "pocket_send_feedback": "Enviar opinión"
+    "pocket_send_feedback": "Enviar opinión",
+    "topstories_empty_state": "Ya estás al día. Vuelve luego y busca más historias de {provider}. ¿No puedes esperar? Selecciona un tema popular y encontrarás más historias interesantes por toda la web.",
+    "manual_migration_explanation": "Prueba Firefox con tus sitios favoritos y marcadores desde otro navegador.",
+    "manual_migration_cancel_button": "No, gracias",
+    "manual_migration_import_button": "Importar ahora"
   },
   "et": {
     "newtab_page_title": "Uus kaart",
     "default_label_loading": "Laadimine…",
     "header_top_sites": "Top saidid",
     "header_highlights": "Esiletõstetud",
     "header_stories": "Top lood",
     "header_stories_from": "allikast",
@@ -2605,56 +2633,62 @@
     "time_label_minute": "{number} ր",
     "time_label_hour": "{number} ժ",
     "time_label_day": "{number} օր"
   },
   "id": {
     "newtab_page_title": "Tab Baru",
     "default_label_loading": "Memuat…",
     "header_top_sites": "Situs Teratas",
-    "header_highlights": "Sorotan",
     "header_stories": "Cerita Utama",
+    "header_visit_again": "Kunjungi Lagi",
+    "header_bookmarks": "Markah Terbaru",
+    "header_recommended_by": "Disarankan oleh {provider}",
+    "header_bookmarks_placeholder": "Anda belum memiliki markah.",
     "header_stories_from": "dari",
     "type_label_visited": "Dikunjungi",
     "type_label_bookmarked": "Dimarkahi",
     "type_label_synced": "Disinkronkan dari perangkat lain",
     "type_label_recommended": "Trending",
     "type_label_open": "Buka",
     "type_label_topic": "Topik",
+    "type_label_now": "Sekarang",
     "menu_action_bookmark": "Markah",
     "menu_action_remove_bookmark": "Hapus Markah",
     "menu_action_copy_address": "Salin Alamat",
     "menu_action_email_link": "Emailkan Tautan…",
     "menu_action_open_new_window": "Buka di Jendela Baru",
     "menu_action_open_private_window": "Buka di Jendela Penjelajahan Pribadi Baru",
     "menu_action_dismiss": "Tutup",
     "menu_action_delete": "Hapus dari Riwayat",
+    "menu_action_pin": "Semat",
+    "menu_action_unpin": "Lepas",
+    "confirm_history_delete_notice_p2": "Tindakan ini tidak bisa diurungkan.",
     "menu_action_save_to_pocket": "Simpan ke Pocket",
     "search_for_something_with": "Cari {search_term} lewat:",
     "search_button": "Cari",
     "search_header": "Pencarian {search_engine_name}",
     "search_web_placeholder": "Cari di Web",
     "search_settings": "Ubah Pengaturan Pencarian",
+    "section_info_option": "Info",
     "welcome_title": "Selamat datang di tab baru",
     "welcome_body": "Firefox akan menggunakan ruang ini untuk menampilkan markah, artikel, video, dan laman yang baru-baru ini dikunjungi, yang paling relevan agar Anda bisa kembali mengunjunginya dengan mudah.",
     "welcome_label": "Mengidentifikasi Sorotan Anda",
     "time_label_less_than_minute": "<1 mnt",
     "time_label_minute": "{number} mnt",
     "time_label_hour": "{number} jam",
     "time_label_day": "{number} hr",
     "settings_pane_button_label": "Ubahsuai laman Tab Baru Anda",
     "settings_pane_header": "Preferensi Tab Baru",
     "settings_pane_body": "Pilih apa yang Anda lihat ketika Anda membuka tab baru.",
     "settings_pane_search_header": "Pencarian",
     "settings_pane_search_body": "Cari Web dari tab baru Anda.",
     "settings_pane_topsites_header": "Situs Teratas",
     "settings_pane_topsites_body": "Mengakses situs web yang paling sering Anda kunjungi.",
     "settings_pane_topsites_options_showmore": "Tampilkan dua baris",
-    "settings_pane_highlights_header": "Sorotan",
-    "settings_pane_highlights_body": "Melihat kembali pada riwayat peramban terbaru dan markah yang baru dibuat.",
     "settings_pane_pocketstories_header": "Cerita Utama",
     "settings_pane_pocketstories_body": "Pocket, bagian dari keluarga Mozilla, akan membantu hubungkan Anda dengan konten berkualitas tinggi yang tak dapat Anda temukan di tempat lain.",
     "settings_pane_done_button": "Selesai",
     "edit_topsites_button_text": "Sunting",
     "edit_topsites_button_label": "Ubahsuai bagian Situs Teratas Anda",
     "edit_topsites_showmore_button": "Tampilkan lainnya",
     "edit_topsites_showless_button": "Tampilkan lebih sedikit",
     "edit_topsites_done_button": "Selesai",
@@ -2848,18 +2882,18 @@
     "topstories_empty_state": "すべて既読です。また後で戻って {provider} からのおすすめ記事をチェックしてください。もし待ちきれないなら、人気のトピックを選択すれば、他にもウェブ上の優れた記事を見つけられます。",
     "manual_migration_explanation": "他のブラウザーから Firefox へあなたのお気に入りのサイトやブックマークを取り込んでみましょう。",
     "manual_migration_cancel_button": "今はしない",
     "manual_migration_import_button": "今すぐインポート"
   },
   "ka": {
     "newtab_page_title": "ახალი ჩანართი",
     "default_label_loading": "იტვირთება…",
-    "header_top_sites": "მთავარი საიტები",
-    "header_stories": "მთავარი სიახლეები",
+    "header_top_sites": "რჩეული საიტები",
+    "header_stories": "რჩეული სტატიები",
     "header_visit_again": "ხელახლა ნახვა",
     "header_bookmarks": "ბოლოს ჩანიშნულები",
     "header_recommended_by": "რეკომენდებულია {provider}-ის მიერ",
     "header_bookmarks_placeholder": "სანიშნეები ჯერ არაა დამატებული.",
     "header_stories_from": "-იდან",
     "type_label_visited": "მონახულებული",
     "type_label_bookmarked": "ჩანიშნული",
     "type_label_synced": "სხვა მოწყობილობიდან დასინქრონებული",
@@ -2893,17 +2927,17 @@
     "time_label_minute": "{number}წთ",
     "time_label_hour": "{number}სთ",
     "time_label_day": "{number}დღე",
     "settings_pane_button_label": "მოირგეთ ახალი ჩანართის გვერდი",
     "settings_pane_header": "ახალი ჩანართის პარამეტრები",
     "settings_pane_body": "აირჩიეთ რისი ხილვა გსურთ ახალი ჩანართის გახსნისას.",
     "settings_pane_search_header": "ძიება",
     "settings_pane_search_body": "ძიება ინტერნეტში ახალი ჩანართიდან.",
-    "settings_pane_topsites_header": "საუკეთესო საიტები",
+    "settings_pane_topsites_header": "რჩეული საიტები",
     "settings_pane_topsites_body": "წვდომა ხშირად მონახულებულ საიტებთან.",
     "settings_pane_topsites_options_showmore": "ორ რიგად ჩვენება",
     "settings_pane_bookmarks_header": "ბოლოს ჩანიშნულები",
     "settings_pane_bookmarks_body": "ახლად შექმნილი სანიშნეები, ერთი ხელის გაწვდენაზე.",
     "settings_pane_visit_again_header": "ხელახლა ნახვა",
     "settings_pane_visit_again_body": "Firefox გაჩვენებთ მონახულებული გვერდების ისტორიიდან იმას, რისი გახსენებაც ან რაზე დაბრუნებაც გენდომებათ.",
     "settings_pane_pocketstories_header": "მთავარი სიახლეები",
     "settings_pane_pocketstories_body": "Pocket არის Mozilla-ს ოჯახის ნაწილი, რომელიც დაგეხმარებათ ისეთი მაღალი ხარისხის კონტენტის მოძიებაში, რომელიც სხვა გზებით, შეიძლება ვერ მოგენახათ.",
@@ -3024,24 +3058,26 @@
   },
   "kk": {
     "newtab_page_title": "Жаңа бет",
     "default_label_loading": "Жүктелуде…",
     "header_top_sites": "Топ сайттар",
     "header_stories": "Топ хикаялар",
     "header_visit_again": "Қайтадан шолу",
     "header_bookmarks": "Соңғы бетбелгілер",
+    "header_recommended_by": "Ұсынушы {provider}",
     "header_bookmarks_placeholder": "Сізде әлі бетбелгілер жоқ.",
     "header_stories_from": "ұсынған",
     "type_label_visited": "Қаралған",
     "type_label_bookmarked": "Бетбелгілерде",
     "type_label_synced": "Басқа құрылғыдан синхрондалған",
     "type_label_recommended": "Әйгілі",
     "type_label_open": "Ашу",
     "type_label_topic": "Тақырып",
+    "type_label_now": "Қазір",
     "menu_action_bookmark": "Бетбелгілерге қосу",
     "menu_action_remove_bookmark": "Бетбелгіні өшіру",
     "menu_action_copy_address": "Адресін көшіріп алу",
     "menu_action_email_link": "Сілтемені эл. поштамен жіберу…",
     "menu_action_open_new_window": "Жаңа терезеде ашу",
     "menu_action_open_private_window": "Жаңа жекелік терезесінде ашу",
     "menu_action_dismiss": "Тайдыру",
     "menu_action_delete": "Тарихтан өшіру",
@@ -3050,16 +3086,17 @@
     "confirm_history_delete_p1": "Бұл парақтың барлық кездесулерін шолу тарихыңыздан өшіруді қалайсыз ба?",
     "confirm_history_delete_notice_p2": "Бұл әрекетті болдырмау мүмкін болмайды.",
     "menu_action_save_to_pocket": "Pocket-ке сақтау",
     "search_for_something_with": "{search_term} ұғымын көмегімен іздеу:",
     "search_button": "Іздеу",
     "search_header": "{search_engine_name} іздеуі",
     "search_web_placeholder": "Интернетте іздеу",
     "search_settings": "Іздеу баптауларын өзгерту",
+    "section_info_option": "Ақпарат",
     "welcome_title": "Жаңа бетке қош келдіңіз",
     "welcome_body": "Firefox бұл орында ең маңызды бетбелгілер, мақалалар, видеолар және жуырда қаралған беттерді көрсетеді, оның көмегімен сіз оларға оңай түрде орала аласыз.",
     "welcome_label": "Ең басты нәрселерді анықтау",
     "time_label_less_than_minute": "<1 минут",
     "time_label_minute": "{number} минут",
     "time_label_hour": "{number} сағат",
     "time_label_day": "{number} күн",
     "settings_pane_button_label": "Жаңа бетті баптаңыз",
@@ -3094,17 +3131,21 @@
     "topsites_form_add_button": "Қосу",
     "topsites_form_save_button": "Сақтау",
     "topsites_form_cancel_button": "Бас тарту",
     "topsites_form_url_validation": "Жарамды сілтеме керек",
     "pocket_read_more": "Әйгілі тақырыптар:",
     "pocket_read_even_more": "Көбірек хикаяларды қарау",
     "pocket_feedback_header": "Интернеттің ең жақсысы, 25 миллион адаммен танылған.",
     "pocket_feedback_body": "Pocket, Mozilla құрамындағы өнім, сізге әдетте табылмауы мүмкін құрамаға байланысуға көмектеседі.",
-    "pocket_send_feedback": "Кері байланыс хабарламасын жіберу"
+    "pocket_send_feedback": "Кері байланыс хабарламасын жіберу",
+    "topstories_empty_state": "Дайын. {provider} ұсынған көбірек мақалаларды алу үшін кейінірек тексеріңіз. Күте алмайсыз ба? Интернеттен көбірек тамаша мақалаларды алу үшін әйгілі теманы таңдаңыз.",
+    "manual_migration_explanation": "Firefox-ты басқа браузерден таңдамалы сайттар және бетбелгілеріңізбен қолданып көріңіз.",
+    "manual_migration_cancel_button": "Жоқ, рахмет",
+    "manual_migration_import_button": "Қазір импорттау"
   },
   "km": {
     "newtab_page_title": "ផ្ទាំង​ថ្មី",
     "default_label_loading": "កំពុង​ផ្ទុក...",
     "header_top_sites": "វិបសាយ​លើ​គេ",
     "header_highlights": "ការ​រំលេច",
     "type_label_visited": "បាន​ចូល​មើល",
     "type_label_bookmarked": "បាន​ចំណាំ",
@@ -3517,17 +3558,17 @@
     "type_label_now": "Sekarang",
     "menu_action_bookmark": "Tandabuku",
     "menu_action_remove_bookmark": "Alihkeluar Tandabuku",
     "menu_action_copy_address": "Salin Alamat",
     "menu_action_email_link": "Pautan E-mel…",
     "menu_action_open_new_window": "Buka dalam Tetingkap Baru",
     "menu_action_open_private_window": "Buka dalam Tetingkap Peribadi Baru",
     "menu_action_dismiss": "Abai",
-    "menu_action_delete": "Hapuskan sejarah",
+    "menu_action_delete": "Hapus daripada Sejarah",
     "menu_action_pin": "Pin",
     "menu_action_unpin": "Nyahpin",
     "confirm_history_delete_p1": "Anda pasti mahu menghapuskan setiap contoh halaman ini daripada sejarah anda?",
     "confirm_history_delete_notice_p2": "Tindakan ini tidak boleh dibatalkan.",
     "menu_action_save_to_pocket": "Simpan ke Pocket",
     "search_for_something_with": "Cari {search_term} dengan:",
     "search_button": "Cari",
     "search_header": "{search_engine_name} Cari",
@@ -3831,48 +3872,51 @@
     "pocket_feedback_body": "Pocket, een onderdeel van de Mozilla-familie, helpt u bij het vinden van inhoud met hoge kwaliteit die u anders misschien niet had kunnen vinden.",
     "pocket_send_feedback": "Feedback verzenden",
     "topstories_empty_state": "U bent weer bij. Kijk later nog eens voor meer topverhalen van {provider}. Kunt u niet wachten? Selecteer een populair onderwerp voor meer geweldige verhalen van het hele web.",
     "manual_migration_explanation": "Probeer Firefox met uw favoriete websites en bladwijzers uit een andere browser.",
     "manual_migration_cancel_button": "Nee bedankt",
     "manual_migration_import_button": "Nu importeren"
   },
   "nn-NO": {
-    "newtab_page_title": "Ny flik",
+    "newtab_page_title": "Ny fane",
     "default_label_loading": "Lastar…",
     "header_top_sites": "Mest vitja",
     "header_stories": "Hovudsakene",
     "header_visit_again": "Bes;kigjen",
     "header_bookmarks": "Nylege bokmerke",
+    "header_recommended_by": "Tilrådd av {provider}",
     "header_bookmarks_placeholder": "Du har ingen bokmerke enno.",
     "header_stories_from": "frå",
     "type_label_visited": "Vitja",
     "type_label_bookmarked": "Bokmerkte",
     "type_label_synced": "Synkronisert frå ei anna eining",
     "type_label_recommended": "Trendar",
     "type_label_open": "Opna",
     "type_label_topic": "Emne",
+    "type_label_now": "No",
     "menu_action_bookmark": "Bokmerke",
     "menu_action_remove_bookmark": "Fjern bokmerke",
     "menu_action_copy_address": "Kopier adresse",
     "menu_action_email_link": "E-postlenke…",
-    "menu_action_open_new_window": "Opna i nytt vindauge",
+    "menu_action_open_new_window": "Opne i nytt vindauge",
     "menu_action_open_private_window": "Opna i eit nytt privat vindauge",
     "menu_action_dismiss": "Avslå",
     "menu_action_delete": "Slett frå historikk",
     "menu_action_pin": "Fest",
     "menu_action_unpin": "L:ys",
     "confirm_history_delete_p1": "Er du sikker på at du vil slette alle førekomstar av denne sida frå historikken din?",
     "confirm_history_delete_notice_p2": "Denne handlinga kan ikkje angrast.",
     "menu_action_save_to_pocket": "Lagre til Pocket",
     "search_for_something_with": "Søk etter {search_term} med:",
     "search_button": "Søk",
     "search_header": "{search_engine_name}",
     "search_web_placeholder": "Søk på nettet",
     "search_settings": "Endra søkjeinnstillingar",
+    "section_info_option": "Info",
     "welcome_title": "Velkomen til ny fane",
     "welcome_body": "Firefox vil bruka denne plassen til å visa deg dei mest relevante bokmerka, artiklane, videoane og sidene du nettopp har vitja, slik at du enkelt kan finna tilbake til dei.",
     "welcome_label": "Identifiserer høgdepunkta dine",
     "time_label_less_than_minute": "<1 min.",
     "time_label_minute": "{number} m",
     "time_label_hour": "{number} t",
     "time_label_day": "{number} d",
     "settings_pane_button_label": "Tilpass sida for Ny fane",
@@ -3895,29 +3939,33 @@
     "edit_topsites_showmore_button": "Vis meir",
     "edit_topsites_showless_button": "Vis mindre",
     "edit_topsites_done_button": "Ferdig",
     "edit_topsites_pin_button": "Fest sida",
     "edit_topsites_unpin_button": "Løys frå denne nettsida",
     "edit_topsites_edit_button": "Rediger denne nettsida",
     "edit_topsites_dismiss_button": "Avvis denne nettsida",
     "edit_topsites_add_button": "Legg til",
-    "topsites_form_add_header": "Ny mest vitja",
+    "topsites_form_add_header": "Ny toppstad",
     "topsites_form_edit_header": "Rediger mest vitja",
     "topsites_form_title_placeholder": "Skriv inn ein tittel",
     "topsites_form_url_placeholder": "Skriv eller lim inn ein URL",
     "topsites_form_add_button": "Legg til",
     "topsites_form_save_button": "Lagre",
     "topsites_form_cancel_button": "Avbryt",
     "topsites_form_url_validation": "Gyldig URL er påkravd",
     "pocket_read_more": "Populære emne:",
     "pocket_read_even_more": "Vis fleire saker",
     "pocket_feedback_header": "Det beste av nettet, sett saman av over 25 millioner menneske.",
     "pocket_feedback_body": "Pocket, ein del av Mozilla-familien, vil hjelpe deg med å finne innhald av høg kvalitet, som du kanskje ikkje ville ha funne elles.",
-    "pocket_send_feedback": "Send tilbakemelding"
+    "pocket_send_feedback": "Send tilbakemelding",
+    "topstories_empty_state": "Det finst ikkje fleire. Kom tilbake seinare for fleire topphistoriar frå {provider}. Kan du ikkje vente? Vel eit populært emne for å finne fleire gode artiklar frå heile nettet.",
+    "manual_migration_explanation": "Prøv Firefox med favorittnettstadane dine og bokmerke frå ein annan nettlesar.",
+    "manual_migration_cancel_button": "Nei takk",
+    "manual_migration_import_button": "Importer no"
   },
   "or": {},
   "pa-IN": {
     "newtab_page_title": "ਨਵੀਂ ਟੈਬ",
     "default_label_loading": "…ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ",
     "header_top_sites": "ਸਿਖਰਲੀਆਂ ਸਾਈਟਾਂ",
     "header_highlights": "ਹਾਈਲਾਈਟ",
     "type_label_visited": "ਖੋਲ੍ਹੀਆਂ",
@@ -4524,16 +4572,17 @@
   },
   "sl": {
     "newtab_page_title": "Nov zavihek",
     "default_label_loading": "Nalaganje …",
     "header_top_sites": "Glavne strani",
     "header_stories": "Glavne vesti",
     "header_visit_again": "Obiščite znova",
     "header_bookmarks": "Nedavni zaznamki",
+    "header_recommended_by": "Priporoča {provider}",
     "header_bookmarks_placeholder": "Nimate še nobenih zaznamkov.",
     "header_stories_from": "od",
     "type_label_visited": "Obiskano",
     "type_label_bookmarked": "Med zaznamki",
     "type_label_synced": "Sinhronizirano z druge naprave",
     "type_label_recommended": "Najbolj priljubljeno",
     "type_label_open": "Odpri",
     "type_label_topic": "Tema",
@@ -4551,16 +4600,17 @@
     "confirm_history_delete_p1": "Ali ste prepričani, da želite izbrisati vse primerke te strani iz zgodovine?",
     "confirm_history_delete_notice_p2": "Tega dejanja ni mogoče razveljaviti.",
     "menu_action_save_to_pocket": "Shrani v Pocket",
     "search_for_something_with": "Išči \"{search_term}\" z iskalnikom:",
     "search_button": "Iskanje",
     "search_header": "Iskanje {search_engine_name}",
     "search_web_placeholder": "Iskanje po spletu",
     "search_settings": "Spremeni nastavitve iskanja",
+    "section_info_option": "Informacije",
     "welcome_title": "Dobrodošli v novem zavihku",
     "welcome_body": "Na tem prostoru bo Firefox prikazoval najustreznejše zaznamke, članke, videoposnetke in nedavno obiskane strani, tako da jih lahko pozneje znova hitro najdete.",
     "welcome_label": "Zbiranje poudarkov",
     "time_label_less_than_minute": "<1 min",
     "time_label_minute": "{number} min",
     "time_label_hour": "{number} ur",
     "time_label_day": "{number} dni",
     "settings_pane_button_label": "Prilagodite stran novega zavihka",
@@ -4658,24 +4708,26 @@
   },
   "sr": {
     "newtab_page_title": "Нови језичак",
     "default_label_loading": "Учитавање…",
     "header_top_sites": "Популарни сајтови",
     "header_stories": "Популарне приче",
     "header_visit_again": "Посетите поново",
     "header_bookmarks": "Недавне забелешке",
+    "header_recommended_by": "Предложио {provider}",
     "header_bookmarks_placeholder": "Још увек немате забелешке.",
     "header_stories_from": "од",
     "type_label_visited": "Посећене",
     "type_label_bookmarked": "Забележено",
     "type_label_synced": "Синхронизовано са другог уређаја",
     "type_label_recommended": "У тренду",
     "type_label_open": "Отвори",
     "type_label_topic": "Тема",
+    "type_label_now": "Сада",
     "menu_action_bookmark": "Забележи",
     "menu_action_remove_bookmark": "Уклони забелешку",
     "menu_action_copy_address": "Копирај адресу",
     "menu_action_email_link": "Веза е-поште…",
     "menu_action_open_new_window": "Отвори у новом прозору",
     "menu_action_open_private_window": "Отвори у новом приватном прозору",
     "menu_action_dismiss": "Занемари",
     "menu_action_delete": "Уклони из историјата",
@@ -4684,16 +4736,17 @@
     "confirm_history_delete_p1": "Да ли сте сигурни да желите да обришете све посете ове странице из ваше историје?",
     "confirm_history_delete_notice_p2": "Ова радња се не може опозвати.",
     "menu_action_save_to_pocket": "Сачувај на Pocket",
     "search_for_something_with": "Претражите {search_term} са:",
     "search_button": "Претражи",
     "search_header": "{search_engine_name} претрага",
     "search_web_placeholder": "Претражујте веб",
     "search_settings": "Измените подешавања претраге",
+    "section_info_option": "Инфо",
     "welcome_title": "Добродошли на нови језичак",
     "welcome_body": "Firefox ће користити овај простор да вам приказује најрелевантне језичке, чланке, видео клипове и странице које сте недавно посетили, како бисте им се лако могли вратити.",
     "welcome_label": "Учитавам ваше истакнуте ставке",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
     "time_label_day": "{number}d",
     "settings_pane_button_label": "Прилагодите страницу новог језичка",
@@ -4728,17 +4781,21 @@
     "topsites_form_add_button": "Додај",
     "topsites_form_save_button": "Сачувај",
     "topsites_form_cancel_button": "Откажи",
     "topsites_form_url_validation": "Исправан URL се захтева",
     "pocket_read_more": "Популарне теме:",
     "pocket_read_even_more": "Погледајте још прича",
     "pocket_feedback_header": "Најбоље од веба од преко 25 милиона кустоса.",
     "pocket_feedback_body": "Pocket, део Mozilla-ине породице, ће вам помоћи да повежете ваш квалитетан садржај који можда не би пронашли другачије.",
-    "pocket_send_feedback": "Пошаљи повратну информацију"
+    "pocket_send_feedback": "Пошаљи повратну информацију",
+    "topstories_empty_state": "Вратите се касније за нове вести {provider}. Не можете дочекати? Изаберите популарну тему да пронађете још занимљивих вести из света.",
+    "manual_migration_explanation": "Пробајте Firefox са вашим омиљеним сајтовима и забелешкама из другог прегледача.",
+    "manual_migration_cancel_button": "Не, хвала",
+    "manual_migration_import_button": "Увези сада"
   },
   "sv-SE": {
     "newtab_page_title": "Ny flik",
     "default_label_loading": "Laddar…",
     "header_top_sites": "Mest besökta",
     "header_stories": "Huvudnyheter",
     "header_visit_again": "Besökt igen",
     "header_bookmarks": "Senaste bokmärken",
@@ -5200,24 +5257,26 @@
   },
   "uk": {
     "newtab_page_title": "Нова вкладка",
     "default_label_loading": "Завантаження…",
     "header_top_sites": "Популярні сайти",
     "header_stories": "Головні новини",
     "header_visit_again": "Відвідати знову",
     "header_bookmarks": "Недавно закладені",
+    "header_recommended_by": "Рекомендовано {provider}",
     "header_bookmarks_placeholder": "У вас ще немає закладок.",
     "header_stories_from": "від",
     "type_label_visited": "Відвідано",
     "type_label_bookmarked": "Закладено",
     "type_label_synced": "Синхронізовано з іншого пристрою",
     "type_label_recommended": "Популярне",
     "type_label_open": "Відкрито",
     "type_label_topic": "Тема",
+    "type_label_now": "Зараз",
     "menu_action_bookmark": "Додати до закладок",
     "menu_action_remove_bookmark": "Вилучити закладку",
     "menu_action_copy_address": "Копіювати адресу",
     "menu_action_email_link": "Надіслати посилання…",
     "menu_action_open_new_window": "Відкрити в новому вікні",
     "menu_action_open_private_window": "Відкрити в приватному вікні",
     "menu_action_dismiss": "Сховати",
     "menu_action_delete": "Видалити з історії",
@@ -5226,16 +5285,17 @@
     "confirm_history_delete_p1": "Ви справді хочете видалити всі записи про цю сторінку з історії?",
     "confirm_history_delete_notice_p2": "Цю дію неможливо відмінити.",
     "menu_action_save_to_pocket": "Зберегти в Pocket",
     "search_for_something_with": "Шукати {search_term} з:",
     "search_button": "Пошук",
     "search_header": "Шукати з {search_engine_name}",
     "search_web_placeholder": "Пошук в Інтернеті",
     "search_settings": "Змінити налаштування пошуку",
+    "section_info_option": "Інфо",
     "welcome_title": "Вітаємо на новій вкладці",
     "welcome_body": "Firefox буде використовувати її для показу найважливіших закладок, статей, відео, а також нещодавно відвіданих сторінок, щоб ви могли з легкістю повернутися до них.",
     "welcome_label": "Визначення обраного",
     "time_label_less_than_minute": "<1 хв",
     "time_label_minute": "{number} хв",
     "time_label_hour": "{number} г",
     "time_label_day": "{number} д",
     "settings_pane_button_label": "Налаштуйте свою сторінку нової вкладки",
@@ -5270,17 +5330,21 @@
     "topsites_form_add_button": "Додати",
     "topsites_form_save_button": "Зберегти",
     "topsites_form_cancel_button": "Скасувати",
     "topsites_form_url_validation": "Необхідна дійсна адреса URL",
     "pocket_read_more": "Популярні теми:",
     "pocket_read_even_more": "Переглянути більше історій",
     "pocket_feedback_header": "Найкраще з Інтернету, відібрано понад 25 мільйонами людей.",
     "pocket_feedback_body": "Pocket, частина сім'ї Mozilla, допоможе підключити вас до якісного вмісту, що ви можете інакше й не знайти.",
-    "pocket_send_feedback": "Надіслати відгук"
+    "pocket_send_feedback": "Надіслати відгук",
+    "topstories_empty_state": "Готово. Перевірте згодом, щоб побачити більше матеріалів від {provider}. Не хочете чекати? Оберіть популярну тему, щоб знайти більше цікавих матеріалів з усього Інтернету.",
+    "manual_migration_explanation": "Спробуйте Firefox з власними улюбленими сайтами і закладками з іншого браузера.",
+    "manual_migration_cancel_button": "Ні, дякую",
+    "manual_migration_import_button": "Імпортувати зараз"
   },
   "ur": {
     "newtab_page_title": "نیا ٹیب",
     "default_label_loading": "لوڈ کر رہا ہے…",
     "header_top_sites": "بہترین سائٹیں",
     "header_stories": "بہترین کہانیاں",
     "header_visit_again": "دوبارہ دورہ کریں",
     "header_bookmarks": "حالیہ نشانیاں",
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -19,23 +19,24 @@ const {Store} = Cu.import("resource://ac
 const {SnippetsFeed} = Cu.import("resource://activity-stream/lib/SnippetsFeed.jsm", {});
 const {SystemTickFeed} = Cu.import("resource://activity-stream/lib/SystemTickFeed.jsm", {});
 const {TelemetryFeed} = Cu.import("resource://activity-stream/lib/TelemetryFeed.jsm", {});
 const {TopSitesFeed} = Cu.import("resource://activity-stream/lib/TopSitesFeed.jsm", {});
 const {TopStoriesFeed} = Cu.import("resource://activity-stream/lib/TopStoriesFeed.jsm", {});
 
 const REASON_ADDON_UNINSTALL = 6;
 
+// For now, we only want to show top stories by default to the following locales
+const showTopStoriesByDefault = ["en-US", "en-CA"].includes(Services.locale.getRequestedLocale());
 // Sections, keyed by section id
 const SECTIONS = new Map([
   ["topstories", {
     feed: TopStoriesFeed,
     prefTitle: "Fetches content recommendations from a configurable content provider",
-     // for now, we only want to show top stories by default to the following locales
-    showByDefault: ["en-US", "en-CA"].includes(Services.locale.getRequestedLocale())
+    showByDefault: showTopStoriesByDefault
   }]
 ]);
 
 const SECTION_FEEDS_CONFIG = Array.from(SECTIONS.entries()).map(entry => {
   const id = entry[0];
   const {feed: Feed, prefTitle, showByDefault: value} = entry[1];
   return {
     name: `section.${id}`,
@@ -70,26 +71,27 @@ const PREFS_CONFIG = new Map([
   }],
   ["telemetry.ping.endpoint", {
     title: "Telemetry server endpoint",
     value: "https://tiles.services.mozilla.com/v4/links/activity-stream"
   }],
   ["feeds.section.topstories.options", {
     title: "Configuration options for top stories feed",
     value: `{
-      "stories_endpoint": "https://getpocket.com/v3/firefox/global-recs?consumer_key=$apiKey",
+      "stories_endpoint": "https://getpocket.com/v3/firefox/global-recs?consumer_key=$apiKey&locale_lang=$locale",
       "stories_referrer": "https://getpocket.com/recommendations",
-      "topics_endpoint": "https://getpocket.com/v3/firefox/trending-topics?consumer_key=$apiKey",
+      "topics_endpoint": "https://getpocket.com/v3/firefox/trending-topics?consumer_key=$apiKey&locale_lang=$locale",
       "read_more_endpoint": "https://getpocket.com/explore/trending?src=ff_new_tab",
       "learn_more_endpoint": "https://getpocket.com/firefox_learnmore?src=ff_newtab",
       "survey_link": "https://www.surveymonkey.com/r/newtabffx",
       "api_key_pref": "extensions.pocket.oAuthConsumerKey",
       "provider_name": "Pocket",
       "provider_icon": "pocket",
-      "provider_description": "pocket_feedback_body"
+      "provider_description": "pocket_feedback_body",
+      "hidden": ${!showTopStoriesByDefault}
     }`
   }],
   ["migrationExpired", {
     title: "Boolean flag that decides whether to show the migration message or not.",
     value: false
   }],
   ["migrationRemainingDays", {
     title: "Number of days to show the manual migration message",
--- a/browser/extensions/activity-stream/lib/ManualMigration.jsm
+++ b/browser/extensions/activity-stream/lib/ManualMigration.jsm
@@ -2,38 +2,48 @@
  * 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/. */
 "use strict";
 
 const {utils: Cu} = Components;
 const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 const {Prefs} = Cu.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
 const MIGRATION_ENDED_EVENT = "Migration:Ended";
+const MS_PER_DAY = 86400000;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "MigrationUtils", "resource:///modules/MigrationUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge", "resource://gre/modules/ProfileAge.jsm");
 
 this.ManualMigration = class ManualMigration {
   constructor() {
     Services.obs.addObserver(this, MIGRATION_ENDED_EVENT);
     this._prefs = new Prefs();
   }
 
   uninit() {
     Services.obs.removeObserver(this, MIGRATION_ENDED_EVENT);
   }
 
-  isMigrationMessageExpired() {
+  async isMigrationMessageExpired() {
+    let profileAge = new ProfileAge();
+    let profileCreationDate = await profileAge.created;
+    let daysSinceProfileCreation = (Date.now() - profileCreationDate) / MS_PER_DAY;
+
+    // We don't want to show the migration message to profiles older than 3 days.
+    if (daysSinceProfileCreation > 3) {
+      return true;
+    }
+
     let migrationLastShownDate = new Date(this._prefs.get("migrationLastShownDate") * 1000);
     let today = new Date();
     // Round down to midnight.
     today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
-
     if (migrationLastShownDate < today) {
       let migrationRemainingDays = this._prefs.get("migrationRemainingDays") - 1;
 
       this._prefs.set("migrationRemainingDays", migrationRemainingDays);
       // .valueOf returns a value that is too large to store so we need to divide by 1000.
       this._prefs.set("migrationLastShownDate", today.valueOf() / 1000);
 
       if (migrationRemainingDays <= 0) {
@@ -46,18 +56,18 @@ this.ManualMigration = class ManualMigra
 
   /**
    * While alreadyExpired is false the migration message is displayed and we also
    * keep checking if we should expire it. Broadcast expiration to store.
    *
    * @param {bool} alreadyExpired Pref flag that is false for the first 3 active days,
    *                              time in which we display the migration message to the user.
    */
-  expireIfNecessary(alreadyExpired) {
-    if (!alreadyExpired && this.isMigrationMessageExpired()) {
+  async expireIfNecessary(alreadyExpired) {
+    if (!alreadyExpired && await this.isMigrationMessageExpired()) {
       this.expireMigration();
     }
   }
 
   expireMigration() {
     this.store.dispatch(ac.SetPref("migrationExpired", true));
   }
 
--- a/browser/extensions/activity-stream/lib/SnippetsFeed.jsm
+++ b/browser/extensions/activity-stream/lib/SnippetsFeed.jsm
@@ -10,16 +10,17 @@ Cu.import("resource://gre/modules/Servic
 const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
   "resource://gre/modules/ProfileAge.jsm");
 
 // Url to fetch snippets, in the urlFormatter service format.
 const SNIPPETS_URL_PREF = "browser.aboutHomeSnippets.updateUrl";
 const TELEMETRY_PREF = "datareporting.healthreport.uploadEnabled";
+const ONBOARDING_FINISHED_PREF = "browser.onboarding.notification.finished";
 
 // Should be bumped up if the snippets content format changes.
 const STARTPAGE_VERSION = 5;
 
 const ONE_WEEK = 7 * 24 * 60 * 60 * 1000;
 
 this.SnippetsFeed = class SnippetsFeed {
   constructor() {
@@ -42,30 +43,33 @@ this.SnippetsFeed = class SnippetsFeed {
   }
   async _refresh() {
     const profileInfo = await this.getProfileInfo();
     const data = {
       snippetsURL: this.snippetsURL,
       version: STARTPAGE_VERSION,
       profileCreatedWeeksAgo: profileInfo.createdWeeksAgo,
       profileResetWeeksAgo: profileInfo.resetWeeksAgo,
-      telemetryEnabled: Services.prefs.getBoolPref(TELEMETRY_PREF)
+      telemetryEnabled: Services.prefs.getBoolPref(TELEMETRY_PREF),
+      onboardingFinished: Services.prefs.getBoolPref(ONBOARDING_FINISHED_PREF)
     };
 
     this.store.dispatch(ac.BroadcastToContent({type: at.SNIPPETS_DATA, data}));
   }
   async init() {
     await this._refresh();
+    Services.prefs.addObserver(ONBOARDING_FINISHED_PREF, this._refresh);
     Services.prefs.addObserver(SNIPPETS_URL_PREF, this._refresh);
     Services.prefs.addObserver(TELEMETRY_PREF, this._refresh);
   }
   uninit() {
-    this.store.dispatch({type: at.SNIPPETS_RESET});
+    Services.prefs.removeObserver(ONBOARDING_FINISHED_PREF, this._refresh);
     Services.prefs.removeObserver(SNIPPETS_URL_PREF, this._refresh);
     Services.prefs.removeObserver(TELEMETRY_PREF, this._refresh);
+    this.store.dispatch({type: at.SNIPPETS_RESET});
   }
   onAction(action) {
     switch (action.type) {
       case at.INIT:
         this.init();
         break;
       case at.FEED_INIT:
         if (action.data === "feeds.snippets") { this.init(); }
--- a/browser/extensions/activity-stream/lib/TopStoriesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TopStoriesFeed.jsm
@@ -23,18 +23,20 @@ this.TopStoriesFeed = class TopStoriesFe
     this.topicsLastUpdated = 0;
   }
 
   init() {
     try {
       const prefs = new Prefs();
       const options = JSON.parse(prefs.get("feeds.section.topstories.options"));
       const apiKey = this._getApiKeyFromPref(options.api_key_pref);
-      this.stories_endpoint = this._produceUrlWithApiKey(options.stories_endpoint, apiKey);
-      this.topics_endpoint = this._produceUrlWithApiKey(options.topics_endpoint, apiKey);
+      const locale = Services.locale.getRequestedLocale();
+      this.stories_endpoint = this._produceFinalEndpointUrl(options.stories_endpoint, apiKey, locale);
+      this.topics_endpoint = this._produceFinalEndpointUrl(options.topics_endpoint, apiKey, locale);
+
       this.read_more_endpoint = options.read_more_endpoint;
       this.stories_referrer = options.stories_referrer;
 
       // TODO https://github.com/mozilla/activity-stream/issues/2902
       const sectionOptions = {
         id: SECTION_ID,
         eventSource: "TOP_STORIES",
         icon: options.provider_icon,
@@ -133,26 +135,27 @@ this.TopStoriesFeed = class TopStoriesFe
   _getApiKeyFromPref(apiKeyPref) {
     if (!apiKeyPref) {
       return apiKeyPref;
     }
 
     return new Prefs().get(apiKeyPref) || Services.prefs.getCharPref(apiKeyPref);
   }
 
-  _produceUrlWithApiKey(url, apiKey) {
+  _produceFinalEndpointUrl(url, apiKey, locale) {
     if (!url) {
       return url;
     }
-
     if (url.includes("$apiKey") && !apiKey) {
       throw new Error(`An API key was specified but none configured: ${url}`);
     }
-
-    return url.replace("$apiKey", apiKey);
+    if (url.includes("$locale") && !locale) {
+      throw new Error(`A locale was specified but none detected: ${url}`);
+    }
+    return url.replace("$apiKey", apiKey).replace("$locale", locale);
   }
 
   // Need to remove parenthesis from image URLs as React will otherwise
   // fail to render them properly as part of the card template.
   _normalizeUrl(url) {
     if (url) {
       return url.replace(/\(/g, "%28").replace(/\)/g, "%29");
     }
--- a/browser/extensions/activity-stream/test/unit/lib/ManualMigration.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/ManualMigration.test.js
@@ -6,16 +6,17 @@ describe("ManualMigration", () => {
   let dispatch;
   let store;
   let instance;
   let globals;
 
   let migrationWizardStub;
   let fakeServices;
   let fakePrefs;
+  let fakeProfileAge;
 
   beforeEach(() => {
     migrationWizardStub = sinon.stub();
     let fakeMigrationUtils = {
       showMigrationWizard: migrationWizardStub,
       MIGRATION_ENTRYPOINT_NEWTAB: "MIGRATION_ENTRYPOINT_NEWTAB"
     };
     fakeServices = {
@@ -23,21 +24,31 @@ describe("ManualMigration", () => {
         addObserver: sinon.stub(),
         removeObserver: sinon.stub()
       }
     };
     fakePrefs = function() {};
     fakePrefs.get = sinon.stub();
     fakePrefs.set = sinon.stub();
 
+    fakeProfileAge = function() {};
+    fakeProfileAge.prototype = {
+      get created() {
+        return new Promise(resolve => {
+          resolve(Date.now());
+        });
+      }
+    };
+
     const {ManualMigration} = injector({"lib/ActivityStreamPrefs.jsm": {Prefs: fakePrefs}});
 
     globals = new GlobalOverrider();
     globals.set("Services", fakeServices);
     globals.set("MigrationUtils", fakeMigrationUtils);
+    globals.set("ProfileAge", fakeProfileAge);
 
     dispatch = sinon.stub();
     store = {dispatch};
     instance = new ManualMigration();
     instance.store = store;
   });
 
   afterEach(() => {
@@ -79,22 +90,22 @@ describe("ManualMigration", () => {
 
       const setStatusStub = sinon.spy(instance, "expireMigration");
       instance.onAction(action);
 
       assert.calledOnce(setStatusStub);
       assert.calledOnce(dispatch);
       assert.calledWithExactly(dispatch, ac.SetPref("migrationExpired", true));
     });
-    it("should set migrationStatus when isMigrationMessageExpired is true", () => {
+    it("should set migrationStatus when isMigrationMessageExpired is true", async () => {
       const setStatusStub = sinon.stub(instance, "expireMigration");
-      const isExpiredStub = sinon.stub(instance, "isMigrationMessageExpired");
-      isExpiredStub.returns(true);
+      sinon.stub(instance, "isMigrationMessageExpired", () => new Promise(resolve => { resolve(true); }));
 
-      instance.expireIfNecessary(false);
+      await instance.expireIfNecessary(false);
+
       assert.calledOnce(setStatusStub);
     });
     it("should call isMigrationMessageExpired if migrationExpired is false", () => {
       const action = {
         type: at.PREFS_INITIAL_VALUES,
         data: {migrationExpired: false}
       };
 
@@ -102,95 +113,136 @@ describe("ManualMigration", () => {
       instance.onAction(action);
 
       assert.calledOnce(stub);
     });
     describe("isMigrationMessageExpired", () => {
       beforeEach(() => {
         instance._prefs = fakePrefs;
       });
-      it("should check migrationLastShownDate (case: today)", () => {
-        const action = {
-          type: at.PREFS_INITIAL_VALUES,
-          data: {migrationExpired: false}
-        };
+      it("should check migrationLastShownDate (case: today)", async () => {
         let today = new Date();
         today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
 
         const migrationSpy = sinon.spy(instance, "isMigrationMessageExpired");
         fakePrefs.get.returns(today);
-        instance.onAction(action);
+        const ret = await instance.isMigrationMessageExpired();
 
         assert.calledOnce(migrationSpy);
         assert.calledOnce(fakePrefs.get);
         assert.calledWithExactly(fakePrefs.get, "migrationLastShownDate");
+        assert.equal(ret, false);
       });
-      it("should return false if lastShownDate is today", () => {
+      it("should return false if lastShownDate is today", async () => {
         let today = new Date();
         today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
 
         const migrationSpy = sinon.spy(instance, "isMigrationMessageExpired");
         fakePrefs.get.returns(today);
-        const ret = instance.isMigrationMessageExpired();
+        const ret = await instance.isMigrationMessageExpired();
 
         assert.calledOnce(migrationSpy);
         assert.calledOnce(fakePrefs.get);
         assert.equal(ret, false);
       });
-      it("should check migrationLastShownDate (case: yesterday)", () => {
+      it("should check migrationLastShownDate (case: yesterday)", async () => {
         const action = {
           type: at.PREFS_INITIAL_VALUES,
           data: {migrationExpired: false}
         };
         let today = new Date();
         let yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
 
         const migrationSpy = sinon.spy(instance, "isMigrationMessageExpired");
         fakePrefs.get.withArgs("migrationLastShownDate").returns(yesterday.valueOf() / 1000);
         fakePrefs.get.withArgs("migrationRemainingDays").returns(4);
-        instance.onAction(action);
+        await instance.onAction(action);
 
         assert.calledOnce(migrationSpy);
         assert.calledTwice(fakePrefs.get);
         assert.calledWithExactly(fakePrefs.get, "migrationLastShownDate");
         assert.calledWithExactly(fakePrefs.get, "migrationRemainingDays");
       });
-      it("should update the migration prefs", () => {
+      it("should update the migration prefs", async () => {
         const action = {
           type: at.PREFS_INITIAL_VALUES,
           data: {migrationExpired: false}
         };
         let today = new Date();
         let yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
         today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
 
         const migrationSpy = sinon.spy(instance, "isMigrationMessageExpired");
         fakePrefs.get.withArgs("migrationLastShownDate").returns(yesterday.valueOf() / 1000);
         fakePrefs.get.withArgs("migrationRemainingDays").returns(4);
-        instance.onAction(action);
+        await instance.onAction(action);
 
         assert.calledOnce(migrationSpy);
         assert.calledTwice(fakePrefs.set);
         assert.calledWithExactly(fakePrefs.set, "migrationRemainingDays", 3);
         assert.calledWithExactly(fakePrefs.set, "migrationLastShownDate", today.valueOf() / 1000);
       });
-      it("should return true if remainingDays reaches 0", () => {
+      it("should return true if remainingDays reaches 0", async () => {
         let today = new Date();
         let yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
 
         const migrationSpy = sinon.spy(instance, "isMigrationMessageExpired");
         fakePrefs.get.withArgs("migrationLastShownDate").returns(yesterday.valueOf() / 1000);
         fakePrefs.get.withArgs("migrationRemainingDays").returns(1);
-        const ret = instance.isMigrationMessageExpired();
+        const ret = await instance.isMigrationMessageExpired();
 
         assert.calledOnce(migrationSpy);
         assert.calledTwice(fakePrefs.set);
         assert.calledWithExactly(fakePrefs.set, "migrationRemainingDays", 0);
         assert.equal(ret, true);
       });
+      it("should return false if profile age < 3", async () => {
+        let today = new Date();
+        let yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
+
+        fakePrefs.get.withArgs("migrationLastShownDate").returns(yesterday.valueOf() / 1000);
+        fakePrefs.get.withArgs("migrationRemainingDays").returns(2);
+        const ret = await instance.isMigrationMessageExpired();
+
+        assert.equal(ret, false);
+      });
+      it("should return true if profile age > 3", async () => {
+        let today = new Date();
+        let someDaysAgo = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 4);
+        fakeProfileAge.prototype = {
+          get created() {
+            return new Promise(resolve => {
+              resolve(someDaysAgo.valueOf());
+            });
+          }
+        };
+
+        fakePrefs.get.withArgs("migrationLastShownDate").returns(someDaysAgo.valueOf() / 1000);
+        fakePrefs.get.withArgs("migrationRemainingDays").returns(2);
+        const ret = await instance.isMigrationMessageExpired();
+
+        assert.equal(ret, true);
+      });
+      it("should return early and not check prefs if profile age > 3", async () => {
+        let today = new Date();
+        let someDaysAgo = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 4);
+        fakeProfileAge.prototype = {
+          get created() {
+            return new Promise(resolve => {
+              resolve(someDaysAgo.valueOf());
+            });
+          }
+        };
+
+        const ret = await instance.isMigrationMessageExpired();
+
+        assert.equal(fakePrefs.get.callCount, 0);
+        assert.equal(fakePrefs.set.callCount, 0);
+        assert.equal(ret, true);
+      });
     });
   });
   it("should have observe as a proxy for setMigrationStatus", () => {
     const setStatusStub = sinon.stub(instance, "expireMigration");
     instance.observe();
 
     assert.calledOnce(setStatusStub);
   });
--- a/browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/SnippetsFeed.test.js
@@ -24,17 +24,22 @@ describe("SnippetsFeed", () => {
   afterEach(() => {
     clock.restore();
     overrider.restore();
     sandbox.restore();
   });
   it("should dispatch a SNIPPETS_DATA action with the right data on INIT", async () => {
     const url = "foo.com/%STARTPAGE_VERSION%";
     sandbox.stub(global.Services.prefs, "getStringPref").returns(url);
-    sandbox.stub(global.Services.prefs, "getBoolPref").returns(true);
+    sandbox.stub(global.Services.prefs, "getBoolPref")
+      .withArgs("datareporting.healthreport.uploadEnabled")
+      .returns(true)
+      .withArgs("browser.onboarding.notification.finished")
+      .returns(false);
+
     const feed = new SnippetsFeed();
     feed.store = {dispatch: sandbox.stub()};
 
     clock.tick(WEEK_IN_MS * 2);
 
     await feed.init();
 
     assert.calledOnce(feed.store.dispatch);
@@ -42,16 +47,17 @@ describe("SnippetsFeed", () => {
     const action = feed.store.dispatch.firstCall.args[0];
     assert.propertyVal(action, "type", at.SNIPPETS_DATA);
     assert.isObject(action.data);
     assert.propertyVal(action.data, "snippetsURL", "foo.com/5");
     assert.propertyVal(action.data, "version", 5);
     assert.propertyVal(action.data, "profileCreatedWeeksAgo", 2);
     assert.propertyVal(action.data, "profileResetWeeksAgo", 1);
     assert.propertyVal(action.data, "telemetryEnabled", true);
+    assert.propertyVal(action.data, "onboardingFinished", false);
   });
   it("should call .init on an INIT aciton", () => {
     const feed = new SnippetsFeed();
     sandbox.stub(feed, "init");
     feed.store = {dispatch: sandbox.stub()};
 
     feed.onAction({type: at.INIT});
     assert.calledOnce(feed.init);
--- a/browser/extensions/activity-stream/test/unit/lib/TopStoriesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TopStoriesFeed.test.js
@@ -10,47 +10,48 @@ describe("Top Stories Feed", () => {
   let TOPICS_UPDATE_TIME;
   let SECTION_ID;
   let instance;
   let clock;
   let globals;
 
   beforeEach(() => {
     FakePrefs.prototype.prefs["feeds.section.topstories.options"] = `{
-      "stories_endpoint": "https://somedomain.org/stories?key=$apiKey",
+      "stories_endpoint": "https://somedomain.org/stories?key=$apiKey&locale=$locale",
       "stories_referrer": "https://somedomain.org/referrer",
-      "topics_endpoint": "https://somedomain.org/topics?key=$apiKey",
+      "topics_endpoint": "https://somedomain.org/topics?key=$apiKey&locale=$locale",
       "survey_link": "https://www.surveymonkey.com/r/newtabffx",
       "api_key_pref": "apiKeyPref",
       "provider_name": "test-provider",
       "provider_icon": "provider-icon",
       "provider_description": "provider_desc"
     }`;
     FakePrefs.prototype.prefs.apiKeyPref = "test-api-key";
 
     globals = new GlobalOverrider();
+    globals.set("Services", {locale: {getRequestedLocale: () => "en-CA"}});
     clock = sinon.useFakeTimers();
 
     ({TopStoriesFeed, STORIES_UPDATE_TIME, TOPICS_UPDATE_TIME, SECTION_ID} = injector({"lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs}}));
     instance = new TopStoriesFeed();
     instance.store = {getState() { return {}; }, dispatch: sinon.spy()};
   });
   afterEach(() => {
     globals.restore();
     clock.restore();
   });
   describe("#init", () => {
     it("should create a TopStoriesFeed", () => {
       assert.instanceOf(instance, TopStoriesFeed);
     });
     it("should initialize endpoints based on prefs", () => {
       instance.onAction({type: at.INIT});
-      assert.equal("https://somedomain.org/stories?key=test-api-key", instance.stories_endpoint);
+      assert.equal("https://somedomain.org/stories?key=test-api-key&locale=en-CA", instance.stories_endpoint);
       assert.equal("https://somedomain.org/referrer", instance.stories_referrer);
-      assert.equal("https://somedomain.org/topics?key=test-api-key", instance.topics_endpoint);
+      assert.equal("https://somedomain.org/topics?key=test-api-key&locale=en-CA", instance.topics_endpoint);
     });
     it("should register section", () => {
       const expectedSectionOptions = {
         id: SECTION_ID,
         eventSource: "TOP_STORIES",
         icon: "provider-icon",
         title: {id: "header_recommended_by", values: {provider: "test-provider"}},
         rows: [],
@@ -100,27 +101,39 @@ describe("Top Stories Feed", () => {
     it("should report error for invalid configuration", () => {
       globals.sandbox.spy(global.Components.utils, "reportError");
       FakePrefs.prototype.prefs["feeds.section.topstories.options"] = "invalid";
       instance.init();
 
       assert.called(Components.utils.reportError);
     });
     it("should report error for missing api key", () => {
-      let fakeServices = {prefs: {getCharPref: sinon.spy()}};
+      let fakeServices = {prefs: {getCharPref: sinon.spy()}, locale: {getRequestedLocale: sinon.spy()}};
       globals.set("Services", fakeServices);
       globals.sandbox.spy(global.Components.utils, "reportError");
       FakePrefs.prototype.prefs["feeds.section.topstories.options"] = `{
         "stories_endpoint": "https://somedomain.org/stories?key=$apiKey",
         "topics_endpoint": "https://somedomain.org/topics?key=$apiKey"
       }`;
       instance.init();
 
       assert.called(Components.utils.reportError);
     });
+    it("should report error for missing locale", () => {
+      let fakeServices = {locale: {getRequestedLocale: sinon.spy()}};
+      globals.set("Services", fakeServices);
+      globals.sandbox.spy(global.Components.utils, "reportError");
+      FakePrefs.prototype.prefs["feeds.section.topstories.options"] = `{
+        "stories_endpoint": "https://somedomain.org/stories?locale=$locale",
+        "topics_endpoint": "https://somedomain.org/topics?locale=$locale"
+      }`;
+      instance.init();
+
+      assert.called(Components.utils.reportError);
+    });
     it("should deregister section", () => {
       instance.onAction({type: at.UNINIT});
       assert.calledOnce(instance.store.dispatch);
       assert.calledWith(instance.store.dispatch, ac.BroadcastToContent({
         type: at.SECTION_DEREGISTER,
         data: SECTION_ID
       }));
     });