diff --git a/core/includes/module.inc b/core/includes/module.inc
index cdd3ddd..55bb667 100644
--- a/core/includes/module.inc
+++ b/core/includes/module.inc
@@ -112,6 +112,7 @@ function system_list_reset() {
   drupal_static_reset('list_themes');
   cache('bootstrap')->delete('system_list');
   cache()->delete('system_info');
+  cache()->delete('project_list');
   // Remove last known theme data state.
   // This causes system_list() to call system_rebuild_theme_data() on its next
   // invocation. When enabling a module that implements hook_system_info_alter()
diff --git a/core/includes/project.inc b/core/includes/project.inc
new file mode 100644
index 0000000..a4ed278
--- /dev/null
+++ b/core/includes/project.inc
@@ -0,0 +1,331 @@
+<?php
+
+/**
+ * @file
+ * API for building lists of installed projects.
+ */
+
+/**
+ * Fetches an array of installed and enabled projects.
+ *
+ * This is only responsible for generating an array of projects (taking into
+ * account projects that include more than one module or theme). Other
+ * information like the specific version and install type (official release,
+ * dev snapshot, etc) is handled later in project_process_version_info() since
+ * that logic is only required when preparing the status report, not for
+ * fetching the available release data.
+ *
+ * This array is fairly expensive to construct, since it involves a lot of disk
+ * I/O, so modules using this list should cache the results.
+ *
+ * @return
+ *   An associative array of currently enabled projects keyed by the
+ *   machine-readable project short name. Each project contains:
+ *   - name: The machine-readable project short name.
+ *   - info: An array with values from the main .info file for this project.
+ *     - name: The human-readable name of the project.
+ *     - package: The package that the project is grouped under.
+ *     - version: The version of the project.
+ *     - project: The Drupal.org project name.
+ *     - datestamp: The date stamp of the project's main .info file.
+ *     - _info_file_ctime: The maximum file change time for all of the .info
+ *       files included in this project.
+ *   - datestamp: The date stamp when the project was released, if known.
+ *   - includes: An associative array containing all projects included with this
+ *     project, keyed by the machine-readable short name with the human-readable
+ *     name as value.
+ *   - project_type: The type of project. Allowed values are 'module' and
+ *     'theme'.
+ *   - project_status: This indicates if the project is enabled and will always
+ *     be TRUE, as the function only returns enabled projects.
+ *   - sub_themes: If the project is a theme it contains an associative array of
+ *     all sub-themes.
+ *   - base_themes: If the project is a theme it contains an associative array
+ *     of all base-themes.
+ *
+ * @see project_process_version_info()
+ * @see update_get_projects()
+ * @see locale_translation_get_projects()
+ */
+function project_list() {
+  if (FALSE && $cached = cache()->get('project_list')) {
+    $projects = $cached->data;
+  }
+  else {
+    $module_data = system_rebuild_module_data();
+    $theme_data = system_rebuild_theme_data();
+    project_process_info_list($projects, $module_data, 'module', TRUE);
+    project_process_info_list($projects, $theme_data, 'theme', TRUE);
+    project_process_info_list($projects, $module_data, 'module', FALSE);
+    project_process_info_list($projects, $theme_data, 'theme', FALSE);
+    // Allow other modules to alter projects before fetching and comparing.
+    drupal_alter('project_list', $projects);
+    // Cache the site's project data for at most 1 hour.
+    cache()->set('project_list', $projects, REQUEST_TIME + 3600);
+  }
+  return $projects;
+}
+
+/**
+ * Populates an array of project data.
+ *
+ * This iterates over a list of the installed modules or themes and groups them
+ * by project and status. A few parts of this function assume that enabled
+ * modules and themes are always processed first, and if disabled modules or
+ * themes are being processed (there is a setting to control if disabled code
+ * should be included or not in the 'Available updates' report), those are only
+ * processed after $projects has been populated with information about the
+ * enabled code. Modules and themes set as hidden are always ignored. This
+ * function also records the latest change time on the .info files for each
+ * module or theme, which is important data which is used when deciding if the
+ * cached available update data should be invalidated.
+ *
+ * @param $projects
+ *   Reference to the array of project data of what's installed on this site.
+ * @param $list
+ *   Array of data to process to add the relevant info to the $projects array.
+ * @param $project_type
+ *   The kind of data in the list. Can be 'module' or 'theme'.
+ * @param $status
+ *   Boolean that controls what status (enabled or disabled) to process out of
+ *   the $list and add to the $projects array.
+ *
+ * @see update_get_projects()
+ */
+function project_process_info_list(&$projects, $list, $project_type, $status) {
+  foreach ($list as $file) {
+     // A disabled base theme of an enabled sub-theme still has all of its code
+    // run by the sub-theme, so we include it in our "enabled" projects list.
+    if ($status && !$file->status && !empty($file->sub_themes)) {
+      foreach ($file->sub_themes as $key => $name) {
+        // Build a list of enabled sub-themes.
+        if ($list[$key]->status) {
+          $file->enabled_sub_themes[$key] = $name;
+        }
+      }
+      // If there are no enabled subthemes, we should ignore this base theme
+      // for the enabled case. If the site is trying to display disabled
+      // themes, we'll catch it then.
+      if (empty($file->enabled_sub_themes)) {
+        continue;
+      }
+    }
+    // Otherwise, just add projects of the proper status to our list.
+    elseif ($file->status != $status) {
+      continue;
+    }
+
+
+    // Skip if the .info file is broken.
+    if (empty($file->info)) {
+      continue;
+    }
+
+    // Skip if it's a hidden module or theme.
+    if (!empty($file->info['hidden'])) {
+      continue;
+    }
+
+    // If the .info doesn't define the 'project', try to figure it out.
+    if (!isset($file->info['project'])) {
+      $file->info['project'] = project_file_get_name($file);
+    }
+
+    // If we still don't know the 'project', give up.
+    if (empty($file->info['project'])) {
+      continue;
+    }
+
+    // If we don't already know it, grab the change time on the .info file
+    // itself. Note: we need to use the ctime, not the mtime (modification
+    // time) since many (all?) tar implementations will go out of their way to
+    // set the mtime on the files it creates to the timestamps recorded in the
+    // tarball. We want to see the last time the file was changed on disk,
+    // which is left alone by tar and correctly set to the time the .info file
+    // was unpacked.
+    if (!isset($file->info['_info_file_ctime'])) {
+      $info_filename = dirname($file->uri) . '/' . $file->name . '.info';
+      $file->info['_info_file_ctime'] = filectime($info_filename);
+    }
+
+    if (!isset($file->info['datestamp'])) {
+      $file->info['datestamp'] = 0;
+    }
+
+    $project_name = $file->info['project'];
+
+    // Figure out what project type we're going to use to display this module
+    // or theme. If the project name is 'drupal', we don't want it to show up
+    // under the usual "Modules" section, we put it at a special "Drupal Core"
+    // section at the top of the report.
+    if ($project_name == 'drupal') {
+      $project_display_type = 'core';
+    }
+    else {
+      $project_display_type = $project_type;
+    }
+    if (empty($status) && empty($file->enabled_sub_themes)) {
+      // If we're processing disabled modules or themes, append a suffix.
+      // However, we don't do this to a a base theme with enabled
+      // subthemes, since we treat that case as if it is enabled.
+      $project_display_type .= '-disabled';
+    }
+    // Add a list of sub-themes that "depend on" the project and a list of base
+    // themes that are "required by" the project.
+    if ($project_name == 'drupal') {
+      // Drupal core is always required, so this extra info would be noise.
+      $sub_themes = array();
+      $base_themes = array();
+    }
+    else {
+      // Add list of enabled sub-themes.
+      $sub_themes = !empty($file->enabled_sub_themes) ? $file->enabled_sub_themes : array();
+      // Add list of base themes.
+      $base_themes = !empty($file->base_themes) ? $file->base_themes : array();
+    }
+    if (!isset($projects[$project_name])) {
+      // Only process this if we haven't done this project, since a single
+      // project can have multiple modules or themes.
+      $projects[$project_name] = array(
+          'name' => $project_name,
+          // Only save attributes from the .info file we care about so we do not
+          // bloat our RAM usage needlessly.
+          'info' => project_filter_info($file->info),
+          'datestamp' => $file->info['datestamp'],
+          'includes' => array($file->name => $file->info['name']),
+          'project_type' => $project_display_type,
+          'project_status' => $status,
+          'sub_themes' => $sub_themes,
+          'base_themes' => $base_themes,
+      );
+    }
+    elseif ($projects[$project_name]['project_type'] == $project_display_type) {
+      // Only add the file we're processing to the 'includes' array for this
+      // project if it is of the same type and status (which is encoded in the
+      // $project_display_type). This prevents listing all the disabled
+      // modules included with an enabled project if we happen to be checking
+      // for disabled modules, too.
+      $projects[$project_name]['includes'][$file->name] = $file->info['name'];
+      $projects[$project_name]['info']['_info_file_ctime'] = max($projects[$project_name]['info']['_info_file_ctime'], $file->info['_info_file_ctime']);
+      $projects[$project_name]['datestamp'] = max($projects[$project_name]['datestamp'], $file->info['datestamp']);
+      if (!empty($sub_themes)) {
+        $projects[$project_name]['sub_themes'] += $sub_themes;
+      }
+      if (!empty($base_themes)) {
+        $projects[$project_name]['base_themes'] += $base_themes;
+      }
+    }
+    elseif (empty($status)) {
+      // If we have a project_name that matches, but the project_display_type
+      // does not, it means we're processing a disabled module or theme that
+      // belongs to a project that has some enabled code. In this case, we add
+      // the disabled thing into a separate array for separate display.
+      $projects[$project_name]['disabled'][$file->name] = $file->info['name'];
+    }
+  }
+}
+
+/**
+ * Determines version and type information for currently installed projects.
+ *
+ * Processes the list of projects on the system to figure out the currently
+ * installed versions, and other information that is required before we can
+ * compare against the available releases to produce the status report.
+ *
+ * @param $projects
+ *   Array of project information from project_list().
+ */
+function project_process_version_info(&$projects) {
+  foreach ($projects as $key => $project) {
+    // Assume an official release until we see otherwise.
+    $install_type = 'official';
+
+    $info = $project['info'];
+
+    if (isset($info['version'])) {
+      // Check for development snapshots
+      if (preg_match('@(dev|HEAD)@', $info['version'])) {
+        $install_type = 'dev';
+      }
+
+      // Figure out what the currently installed major version is. We need
+      // to handle both contribution (e.g. "5.x-1.3", major = 1) and core
+      // (e.g. "5.1", major = 5) version strings.
+      $matches = array();
+      if (preg_match('/^(\d+\.x-)?(\d+)\..*$/', $info['version'], $matches)) {
+        $info['major'] = $matches[2];
+      }
+      elseif (!isset($info['major'])) {
+        // This would only happen for version strings that don't follow the
+        // drupal.org convention. We let contribs define "major" in their
+        // .info in this case, and only if that's missing would we hit this.
+        $info['major'] = -1;
+      }
+    }
+    else {
+      // No version info available at all.
+      $install_type = 'unknown';
+      $info['version'] = t('Unknown');
+      $info['major'] = -1;
+    }
+
+    // Finally, save the results we care about into the $projects array.
+    $projects[$key]['existing_version'] = $info['version'];
+    $projects[$key]['existing_major'] = $info['major'];
+    $projects[$key]['install_type'] = $install_type;
+  }
+}
+
+/**
+ * Determines what project a given file object belongs to.
+ *
+ * @param $file
+ *   A file object as returned by system_get_files_database().
+ *
+ * @return
+ *   The canonical project short name.
+ *
+ * @see system_get_files_database()
+ */
+function project_file_get_name($file) {
+  $project_name = '';
+  if (isset($file->info['project'])) {
+    $project_name = $file->info['project'];
+  }
+  elseif (isset($file->info['package']) && (strpos($file->info['package'], 'Core') === 0)) {
+    $project_name = 'drupal';
+  }
+  return $project_name;
+}
+
+/**
+ * Filters the project .info data to only save attributes we need.
+ *
+ * The saved attributes will be a predefined list plus all the attributes
+ * whose name start with 'project'.
+ *
+ * @param array $info
+ *   Array of .info file data as returned by drupal_parse_info_file().
+ *
+ * @return
+ *   Array of .info file data we need for the project list.
+ *
+ * @see project_process_info_list()
+ */
+function project_filter_info($info) {
+  $whitelist = array(
+    '_info_file_ctime',
+    'datestamp',
+    'major',
+    'name',
+    'package',
+    'version',
+  );
+  $return = array();
+  foreach ($info as $name => $data) {
+    if (in_array($name, $whitelist) || strpos($name, 'project') === 0) {
+      $return[$name] = $data;
+    }
+  }
+  return $return;
+}
diff --git a/core/lib/Drupal/Component/Gettext/PoItem.php b/core/lib/Drupal/Component/Gettext/PoItem.php
index 599a58a..6e6139a 100644
--- a/core/lib/Drupal/Component/Gettext/PoItem.php
+++ b/core/lib/Drupal/Component/Gettext/PoItem.php
@@ -265,6 +265,7 @@ private function formatSingular() {
    * Formats a string for output on multiple lines.
    */
   private function formatString($string) {
+if (!is_string($string)) {debug($string);}
     // Escape characters for processing.
     $string = addcslashes($string, "\0..\37\\\"");
 
diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php
index 44bfabe..e994c67 100644
--- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php
+++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php
@@ -56,7 +56,7 @@ function testUpdateProjects() {
     // Check if interface translation data is collected from hook_info.
     $projects = locale_translation_project_list();
     $this->assertFalse(isset($projects['locale_test_translate']), 'Hidden module not found');
-    $this->assertEqual($projects['locale_test']['info']['interface translation server pattern'], 'core/modules/locale/test/test.%language.po', 'Interface translation parameter found in project info.');
+    $this->assertEqual($projects['locale_test']['info']['project translation server pattern'], 'core/modules/locale/test/test.%language.po', 'Interface translation parameter found in project info.');
     $this->assertEqual($projects['locale_test']['name'] , 'locale_test', format_string('%key found in project info.', array('%key' => 'interface translation project')));
   }
 
@@ -88,8 +88,8 @@ function testUpdateProjectsHidden() {
    * The translation status process by default checks the status of the
    * installed projects. For testing purpose a predefined set of modules with
    * fixed file names and release versions is used. This custom project
-   * definition is applied using a hook_locale_translation_projects_alter
-   * implementation in the locale_test module.
+   * definition is applied using a hook_project_list_alter implementation in the
+   * locale_test module.
    *
    * This test generates a set of local and remote translation files in their
    * respective local and remote translation directory. The test checks whether
diff --git a/core/modules/locale/locale.api.php b/core/modules/locale/locale.api.php
index 0db2091..e60154f 100644
--- a/core/modules/locale/locale.api.php
+++ b/core/modules/locale/locale.api.php
@@ -28,8 +28,7 @@
  * Example .info.yml file properties for a custom module with a po file located
  * in the module's folder.
  * @code
- * interface translation project = example_module
- * interface translation server pattern = modules/custom/example_module/%project-%version.%language.po
+ * project translation server pattern = modules/custom/example_module/%project-%version.%language.po
  * @endcode
  *
  * Streamwrappers can be used in the server pattern definition. The interface
@@ -37,29 +36,29 @@
  * using the "translations://" streamwrapper. But also other streamwrappers can
  * be used.
  * @code
- * interface translation server pattern = translations://%project-%version.%language.po
+ * project translation server pattern = translations://%project-%version.%language.po
  * @endcode
  * @code
- * interface translation server pattern = public://translations/%project-%version.%language.po
+ * project translation server pattern = public://translations/%project-%version.%language.po
  * @endcode
  *
  * Multiple custom modules or themes sharing the same po file should have
  * matching definitions. Such as modules and sub-modules or multiple modules in
  * the same project/code tree. Both "interface translation project" and
- * "interface translation server pattern" definitions of these modules should match.
+ * "project translation server pattern" definitions of these modules should match.
  *
  * Example .info.yml file properties for a custom module with a po file located
  * on a remote translation server.
  * @code
  * interface translation project = example_module
- * interface translation server pattern = http://example.com/files/translations/%core/%project/%project-%version.%language.po
+ * project translation server pattern = http://example.com/files/translations/%core/%project/%project-%version.%language.po
  * @endcode
  *
  * Custom themes, features and distributions can implement these .info.yml file
  * properties in their .info.yml file too.
  *
  * To change the interface translation settings of modules and themes hosted at
- * drupal.org use hook_locale_translation_projects_alter(). Possible changes
+ * drupal.org use hook_project_list_alter(). Possible changes
  * include changing the po file location (server pattern) or removing the
  * project from the translation update list.
  *
@@ -67,7 +66,7 @@
  * - "interface translation project": project name. Required.
  *   Name of the project a (sub-)module belongs to. Multiple modules sharing
  *   the same project name will be listed as one the translation status list.
- * - "interface translation server pattern": URL of the .po translation files
+ * - "project translation server pattern": URL of the .po translation files
  *   used to download the files from. The URL contains tokens which will be
  *   replaced by appropriate values. The file can be locate both at a local
  *   relative path, a local absolute path and a remote server location.
@@ -80,46 +79,3 @@
  *
  * @} End of "defgroup interface_translation_properties".
  */
-
-/**
- * @addtogroup hooks
- * @{
- */
-
-/**
- * Alter the list of projects to be updated by locale's interface translation.
- *
- * Locale module attempts to update the translation of those modules returned
- * by update_get_projects(). Using this hook, the data returned by
- * update_get_projects() can be altered or extended.
- *
- * Modules or distributions that use a dedicated translation server should use
- * this hook to specify the interface translation server pattern, or to add
- * additional custom/non-Drupal.org modules to the list of modules known to
- * locale.
- * - "interface translation server pattern": URL of the .po translation files
- *   used to download the files from. The URL contains tokens which will be
- *   replaced by appropriate values.
- * The following tokens are available for the server pattern:
- * - "%core": Core version. Value example: "8.x".
- * - "%project": Project name. Value examples: "drupal", "media_gallery".
- * - "%version": Project version release. Value examples: "8.1", "8.x-1.0".
- * - "%language": Language code. Value examples: "fr", "pt-pt".
- *
- * @param array $projects
- *   Project data as returned by update_get_projects().
- *
- * @see locale_translation_project_list().
- */
-function hook_locale_translation_projects_alter(&$projects) {
-  // The translations are located at a custom translation sever.
-  $projects['existing_project'] = array(
-    'info' => array(
-      'interface translation server pattern' => 'http://example.com/files/translations/%core/%project/%project-%version.%language.po',
-    ),
-  );
-}
-
-/**
- * @} End of "addtogroup hooks".
- */
diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc
index 3cbaac5..2581d7e 100644
--- a/core/modules/locale/locale.compare.inc
+++ b/core/modules/locale/locale.compare.inc
@@ -5,6 +5,9 @@
  * The API for comparing project translation status with available translation.
  */
 
+// Include project API.
+require_once DRUPAL_ROOT . '/core/includes/project.inc';
+
 use Drupal\Core\Cache;
 
 /**
@@ -45,12 +48,7 @@ function locale_translation_flush_projects() {
  *   - "status": Project status, 1 = enabled.
  */
 function locale_translation_build_projects() {
-  // This function depends on Update module. We degrade gracefully.
-  if (!Drupal::moduleHandler()->moduleExists('update')) {
-    return array();
-  }
-
-  // Get the project list based on .info.yml files.
+  // Get the project list based on .info files.
   $projects = locale_translation_project_list();
 
   // Mark all previous projects as disabled and store new project data.
@@ -63,7 +61,7 @@ function locale_translation_build_projects() {
   $default_server = locale_translation_default_translation_server();
 
   // If project is a dev release, or core, find the latest available release.
-  $project_updates = update_get_available(TRUE);
+  $project_updates = module_exists('update') ? update_get_available(TRUE) : array();
   foreach ($projects as $name => $data) {
     if (isset($project_updates[$name]['releases']) && $project_updates[$name]['project_status'] != 'not-fetched') {
       // Find out if a dev version is installed.
@@ -94,7 +92,7 @@ function locale_translation_build_projects() {
       'core' => isset($data['info']['core']) ? $data['info']['core'] : DRUPAL_CORE_COMPATIBILITY,
       // A project can provide the path and filename pattern to download the
       // gettext file. Use the default if not.
-      'server_pattern' => isset($data['info']['interface translation server pattern']) && $data['info']['interface translation server pattern'] ? $data['info']['interface translation server pattern'] : $default_server['pattern'],
+      'server_pattern' => isset($data['info']['project translation server pattern']) && $data['info']['project translation server pattern'] ? $data['info']['project translation server pattern'] : $default_server['pattern'],
       'status' => !empty($data['project_status']) ? 1 : 0,
     );
     $project = (object) $data;
@@ -122,73 +120,25 @@ function locale_translation_build_projects() {
 /**
  * Fetch an array of projects for translation update.
  *
+ * Filter out projects that have the info property
+ * 'project translation server pattern' set to FALSE.
+ *
  * @return array
  *   Array of project data including .info.yml file data.
  */
 function locale_translation_project_list() {
-  // This function depends on Update module. We degrade gracefully.
-  if (!Drupal::moduleHandler()->moduleExists('update')) {
-    return array();
-  }
-
   $projects = &drupal_static(__FUNCTION__, array());
   if (empty($projects)) {
-    module_load_include('compare.inc', 'update');
-    $config = config('locale.settings');
-    $projects = array();
-
-    $additional_whitelist = array(
-      'interface translation project',
-      'interface translation server pattern',
-    );
-    $module_data = _locale_translation_prepare_project_list(system_rebuild_module_data(), 'module');
-    $theme_data = _locale_translation_prepare_project_list(system_rebuild_theme_data(), 'theme');
-    update_process_info_list($projects, $module_data, 'module', TRUE, $additional_whitelist);
-    update_process_info_list($projects, $theme_data, 'theme', TRUE, $additional_whitelist);
-    if ($config->get('translation.check_disabled_modules')) {
-      update_process_info_list($projects, $module_data, 'module', FALSE, $additional_whitelist);
-      update_process_info_list($projects, $theme_data, 'theme', FALSE, $additional_whitelist);
-    }
-
-    // Allow other modules to alter projects before fetching and comparing.
-    drupal_alter('locale_translation_projects', $projects);
+    $include_disabled = config('locale.settings')->get('translation.check_disabled_modules');
+    $projects = array_filter(project_list(), function ($project) use ($include_disabled) {
+      return ($include_disabled || !empty($project['project_status'])) &&
+        (!isset($project['info']['project translation server pattern']) || $project['info']['project translation server pattern']);
+    });
   }
   return $projects;
 }
 
 /**
- * Prepare module and theme data.
- *
- * Modify .info.yml file data before it is processed by update_process_info_list().
- * In order for update_process_info_list() to recognize a project, it requires
- * the 'project' parameter in the .info.yml file data.
- *
- * Custom modules or themes can bring their own gettext translation file. To
- * enable import of this file the module or theme defines "interface translation
- * project = myproject" in its .info.yml file. This function will add a project
- * "myproject" to the info data.
- *
- * @param array $data
- *   Array of .info.yml file data.
- * @param string $type
- *   The project type. i.e. module, theme.
- *
- * @return array
- *   Array of .info.yml file data.
- */
-function _locale_translation_prepare_project_list($data, $type) {
-  foreach ($data as $name => $file) {
-    // Include interface translation projects. To allow
-    // update_process_info_list() to identify this as a project the 'project'
-    // property is filled with the 'interface translation project' value.
-    if (isset($file->info['interface translation project'])) {
-      $data[$name]->info['project'] = $file->info['interface translation project'];
-    }
-  }
-  return $data;
-}
-
-/**
  * Retrieve data for default server.
  *
  * @return array
@@ -324,7 +274,7 @@ function _locale_translation_batch_status_operations($projects, $langcodes, $opt
  * translation directory is checked for the po file. When a server_pattern is
  * defined the specified location is checked. The server_pattern can be set in
  * the module's .info.yml file or by using
- * hook_locale_translation_projects_alter().
+ * the module's .info file or by using hook_project_list_alter().
  *
  * @param array $projects
  *   Array of project names for which to check the state of translation files.
diff --git a/core/modules/locale/locale.translation.inc b/core/modules/locale/locale.translation.inc
index 3c77ff0..5081c47 100644
--- a/core/modules/locale/locale.translation.inc
+++ b/core/modules/locale/locale.translation.inc
@@ -153,7 +153,7 @@ function locale_translation_build_sources($projects = array(), $langcodes = arra
  * po file we are looking for. The file name defaults to
  * %project-%version.%language.po. Per project this value can be overridden
  * using the server_pattern directive in the module's .info.yml file or by using
- * hook_locale_translation_projects_alter().
+ * locale_test_project_list_alter().
  *
  * @param object $source
  *   Translation source object.
diff --git a/core/modules/locale/tests/modules/locale_test/locale_test.module b/core/modules/locale/tests/modules/locale_test/locale_test.module
index 1ba430c..71c43ae 100644
--- a/core/modules/locale/tests/modules/locale_test/locale_test.module
+++ b/core/modules/locale/tests/modules/locale_test/locale_test.module
@@ -25,7 +25,7 @@ function locale_test_system_info_alter(&$info, $file, $type) {
 }
 
 /**
- * Implements hook_locale_translation_projects_alter().
+ * Implements hook_project_list_alter().
  *
  * The translation status process by default checks the status of the installed
  * projects. This function replaces the data of the installed modules by a
@@ -36,7 +36,7 @@ function locale_test_system_info_alter(&$info, $file, $type) {
  * The "locale.test_projects_alter" state variable must be set by the
  * test script in order for this hook to take effect.
  */
-function locale_test_locale_translation_projects_alter(&$projects) {
+function locale_test_project_list_alter(&$projects) {
   if (Drupal::state()->get('locale.test_projects_alter')) {
 
     // Instead of the default ftp.drupal.org we use the file system of the test
@@ -51,12 +51,13 @@ function locale_test_locale_translation_projects_alter(&$projects) {
         'name' => 'contrib_module_one',
         'info' => array (
           'name' => 'Contributed module one',
-          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
+          'project translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
           'package' => 'Other',
           'version' => '8.x-1.1',
           'project' => 'contrib_module_one',
           'datestamp' => '1344471537',
           '_info_file_ctime' => 1348767306,
+          'project status url' => FALSE,
         ),
         'datestamp' => '1344471537',
         'project_type' => 'module',
@@ -66,12 +67,13 @@ function locale_test_locale_translation_projects_alter(&$projects) {
         'name' => 'contrib_module_two',
         'info' => array (
           'name' => 'Contributed module two',
-          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
+          'project translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
           'package' => 'Other',
           'version' => '8.x-2.0-beta4',
           'project' => 'contrib_module_two',
           'datestamp' => '1344471537',
           '_info_file_ctime' => 1348767306,
+          'project status url' => FALSE,
         ),
         'datestamp' => '1344471537',
         'project_type' => 'module',
@@ -81,12 +83,13 @@ function locale_test_locale_translation_projects_alter(&$projects) {
         'name' => 'contrib_module_three',
         'info' => array (
           'name' => 'Contributed module three',
-          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
+          'project translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
           'package' => 'Other',
           'version' => '8.x-1.0',
           'project' => 'contrib_module_three',
           'datestamp' => '1344471537',
           '_info_file_ctime' => 1348767306,
+          'project status url' => FALSE,
         ),
         'datestamp' => '1344471537',
         'project_type' => 'module',
@@ -96,13 +99,12 @@ function locale_test_locale_translation_projects_alter(&$projects) {
         'name' => 'locale_test',
         'info' => array (
           'name' => 'Locale test',
-          'interface translation project' => 'locale_test',
-          'interface translation server pattern' => 'core/modules/locale/tests/test.%language.po',
+          'project translation server pattern' => 'core/modules/locale/tests/test.%language.po',
           'package' => 'Other',
           'version' => NULL,
-          'project' => 'locale_test',
           '_info_file_ctime' => 1348767306,
           'datestamp' => 0,
+          'project status url' => FALSE,
         ),
         'datestamp' => 0,
         'project_type' => 'module',
@@ -112,13 +114,12 @@ function locale_test_locale_translation_projects_alter(&$projects) {
         'name' => 'custom_module_one',
         'info' => array (
           'name' => 'Custom module one',
-          'interface translation project' => 'custom_module_one',
-          'interface translation server pattern' => 'translations://custom_module_one.%language.po',
+          'project translation server pattern' => 'translations://custom_module_one.%language.po',
           'package' => 'Other',
           'version' => NULL,
-          'project' => 'custom_module_one',
           '_info_file_ctime' => 1348767306,
           'datestamp' => 0,
+          'project status url' => FALSE,
         ),
         'datestamp' => 0,
         'project_type' => 'module',
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 36e9f77..abcbcc7 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -1490,6 +1490,101 @@ function hook_system_info_alter(&$info, $file, $type) {
 }
 
 /**
+ * Alter the list of projects before fetching data and comparing versions.
+ *
+ * Most modules will never need to implement this hook. It is for advanced
+ * interaction with the Update Manager module. The primary use-case for this
+ * hook is to add projects to the list; for example, to provide update status
+ * data on disabled modules and themes. A contributed module might want to hide
+ * projects from the list; for example, if there is a site-specific module that
+ * doesn't have any official releases, that module could remove itself from this
+ * list to avoid "No available releases found" warnings on the available updates
+ * report. In rare cases, a module might want to alter the data associated with
+ * a project already in the list.
+ *
+ * If a project is not intended to be checked for code updates, add this data
+ * data in the info file: 'project status url = FALSE'
+ *
+ * Locale module attempts to update the translation of those modules returned
+ * by update_get_projects(). Using this hook, the data returned by
+ * update_get_projects() can be altered or extended.
+ *
+ * Modules or distributions that use a dedicated translation server should use
+ * this hook to specify the project translation server pattern, or to add
+ * additional custom/non-Drupal.org modules to the list of modules known to
+ * locale.
+ * - "project translation server pattern": URL of the .po translation files
+ *   used to download the files from. The URL contains tokens which will be
+ *   replaced by appropriate values.
+ * The following tokens are available for the server pattern:
+ * - "%core": Core version. Value example: "8.x".
+ * - "%project": Project name. Value examples: "drupal", "media_gallery".
+ * - "%version": Project version release. Value examples: "8.1", "8.x-1.0".
+ * - "%language": Language code. Value examples: "fr", "pt-pt".
+ *
+ * If a project is not intended to be checked for translation updates, add
+ * this line in the info file: 'project translation server pattern = FALSE'
+ *
+ * @param $projects
+ *   Reference to an array of the projects installed on the system. This
+ *   includes all the metadata documented in the comments below for each project
+ *   (either module or theme) that is currently enabled. The array is initially
+ *   populated inside project_list() with the help of
+ *   project_process_info_list(), so look there for examples of how to populate
+ *   the array with real values.
+ *
+ * @see project_list()
+ * @see project_process_info_list()
+ * @see update_get_projects()
+ * @see locale_translation_project_list()
+ */
+function hook_project_list_alter(&$projects) {
+  // Hide a site-specific module from the list.
+  unset($projects['site_specific_module']);
+
+  // Add a disabled module to the list.
+  // The key for the array should be the machine-readable project "short name".
+  $projects['disabled_project_name'] = array(
+    // Machine-readable project short name (same as the array key above).
+    'name' => 'disabled_project_name',
+    // Array of values from the main .info file for this project.
+    'info' => array(
+      'name' => 'Some disabled module',
+      'description' => 'A module not enabled on the site that you want to see in the available updates report.',
+      'version' => '8.x-1.0',
+      'core' => '8.x',
+      // The maximum file change time (the "ctime" returned by the filectime()
+      // PHP method) for all of the .info files included in this project.
+      '_info_file_ctime' => 1243888165,
+    ),
+    // The date stamp when the project was released, if known. If the disabled
+    // project was an officially packaged release from drupal.org, this will
+    // be included in the .info file as the 'datestamp' field. This only
+    // really matters for development snapshot releases that are regenerated,
+    // so it can be left undefined or set to 0 in most cases.
+    'datestamp' => 1243888185,
+    // Any modules (or themes) included in this project. Keyed by machine-
+    // readable "short name", value is the human-readable project name printed
+    // in the UI.
+    'includes' => array(
+      'disabled_project' => 'Disabled module',
+      'disabled_project_helper' => 'Disabled module helper module',
+      'disabled_project_foo' => 'Disabled module foo add-on module',
+    ),
+    // Does this project contain a 'module', 'theme', 'disabled-module', or
+    // 'disabled-theme'?
+    'project_type' => 'disabled-module',
+  );
+
+  // The translations are located at a custom translation sever.
+  $projects['existing_project'] = array(
+    'info' => array(
+      'project translation server pattern' => 'http://example.com/files/translations/%core/%project/%project-%version.%language.po',
+    ),
+  );
+}
+
+/**
  * Define user permissions.
  *
  * This hook can supply permissions that the module defines, so that they
diff --git a/core/modules/update/update.api.php b/core/modules/update/update.api.php
index b5621dd..78d8d84 100644
--- a/core/modules/update/update.api.php
+++ b/core/modules/update/update.api.php
@@ -11,69 +11,7 @@
  */
 
 /**
- * Alter the list of projects before fetching data and comparing versions.
- *
- * Most modules will never need to implement this hook. It is for advanced
- * interaction with the Update Manager module. The primary use-case for this
- * hook is to add projects to the list; for example, to provide update status
- * data on disabled modules and themes. A contributed module might want to hide
- * projects from the list; for example, if there is a site-specific module that
- * doesn't have any official releases, that module could remove itself from this
- * list to avoid "No available releases found" warnings on the available updates
- * report. In rare cases, a module might want to alter the data associated with
- * a project already in the list.
- *
- * @param $projects
- *   Reference to an array of the projects installed on the system. This
- *   includes all the metadata documented in the comments below for each project
- *   (either module or theme) that is currently enabled. The array is initially
- *   populated inside update_get_projects() with the help of
- *   update_process_info_list(), so look there for examples of how to populate
- *   the array with real values.
- *
- * @see update_get_projects()
- * @see update_process_info_list()
- */
-function hook_update_projects_alter(&$projects) {
-  // Hide a site-specific module from the list.
-  unset($projects['site_specific_module']);
-
-  // Add a disabled module to the list.
-  // The key for the array should be the machine-readable project "short name".
-  $projects['disabled_project_name'] = array(
-    // Machine-readable project short name (same as the array key above).
-    'name' => 'disabled_project_name',
-    // Array of values from the main .info.yml file for this project.
-    'info' => array(
-      'name' => 'Some disabled module',
-      'description' => 'A module not enabled on the site that you want to see in the available updates report.',
-      'version' => '8.x-1.0',
-      'core' => '8.x',
-      // The maximum file change time (the "ctime" returned by the filectime()
-      // PHP method) for all of the .info.yml files included in this project.
-      '_info_file_ctime' => 1243888165,
-    ),
-    // The date stamp when the project was released, if known. If the disabled
-    // project was an officially packaged release from drupal.org, this will
-    // be included in the .info.yml file as the 'datestamp' field. This only
-    // really matters for development snapshot releases that are regenerated,
-    // so it can be left undefined or set to 0 in most cases.
-    'datestamp' => 1243888185,
-    // Any modules (or themes) included in this project. Keyed by machine-
-    // readable "short name", value is the human-readable project name printed
-    // in the UI.
-    'includes' => array(
-      'disabled_project' => 'Disabled module',
-      'disabled_project_helper' => 'Disabled module helper module',
-      'disabled_project_foo' => 'Disabled module foo add-on module',
-    ),
-    // Does this project contain a 'module', 'theme', 'disabled-module', or
-    // 'disabled-theme'?
-    'project_type' => 'disabled-module',
-  );
-}
 
-/**
  * Alter the information about available updates for projects.
  *
  * @param $projects
diff --git a/core/modules/update/update.compare.inc b/core/modules/update/update.compare.inc
index c8f5958..2c1b852 100644
--- a/core/modules/update/update.compare.inc
+++ b/core/modules/update/update.compare.inc
@@ -5,314 +5,36 @@
  * Code required only when comparing available updates to existing data.
  */
 
+// Include project API.
+require_once DRUPAL_ROOT . '/core/includes/project.inc';
+
 /**
  * Fetches an array of installed and enabled projects.
  *
- * This is only responsible for generating an array of projects (taking into
- * account projects that include more than one module or theme). Other
- * information like the specific version and install type (official release,
- * dev snapshot, etc) is handled later in update_process_project_info() since
- * that logic is only required when preparing the status report, not for
- * fetching the available release data.
- *
- * This array is fairly expensive to construct, since it involves a lot of disk
- * I/O, so we store the results. However, since this is not the data about
- * available updates fetched from the network, it is acceptable to invalidate it
- * somewhat quickly. If we keep this data for very long, site administrators are
- * more likely to see incorrect results if they upgrade to a newer version of a
- * module or theme but do not visit certain pages that automatically clear this
- * data.
+ * Filter out projects that have the info property
+ * 'project status url' set to FALSE.
  *
  * @return
  *   An associative array of currently enabled projects keyed by the
- *   machine-readable project short name. Each project contains:
- *   - name: The machine-readable project short name.
- *   - info: An array with values from the main .info.yml file for this project.
- *     - name: The human-readable name of the project.
- *     - package: The package that the project is grouped under.
- *     - version: The version of the project.
- *     - project: The Drupal.org project name.
- *     - datestamp: The date stamp of the project's main .info.yml file.
- *     - _info_file_ctime: The maximum file change time for all of the .info.yml
- *       files included in this project.
- *   - datestamp: The date stamp when the project was released, if known.
- *   - includes: An associative array containing all projects included with this
- *     project, keyed by the machine-readable short name with the human-readable
- *     name as value.
- *   - project_type: The type of project. Allowed values are 'module' and
- *     'theme'.
- *   - project_status: This indicates if the project is enabled and will always
- *     be TRUE, as the function only returns enabled projects.
- *   - sub_themes: If the project is a theme it contains an associative array of
- *     all sub-themes.
- *   - base_themes: If the project is a theme it contains an associative array
- *     of all base-themes.
+ *   machine-readable project short name as defined by project_list().
  *
- * @see update_process_project_info()
+ * @see project_process_version_info()
  * @see update_calculate_project_data()
  * @see update_project_storage()
  */
 function update_get_projects() {
   $projects = &drupal_static(__FUNCTION__, array());
   if (empty($projects)) {
-    // Retrieve the projects from storage, if present.
-    $projects = update_project_storage('update_project_projects');
-    if (empty($projects)) {
-      // Still empty, so we have to rebuild.
-      $module_data = system_rebuild_module_data();
-      $theme_data = system_rebuild_theme_data();
-      update_process_info_list($projects, $module_data, 'module', TRUE);
-      update_process_info_list($projects, $theme_data, 'theme', TRUE);
-      if (config('update.settings')->get('check.disabled_extensions')) {
-        update_process_info_list($projects, $module_data, 'module', FALSE);
-        update_process_info_list($projects, $theme_data, 'theme', FALSE);
-      }
-      // Allow other modules to alter projects before fetching and comparing.
-      drupal_alter('update_projects', $projects);
-      // Store the site's project data for at most 1 hour.
-      Drupal::keyValueExpirable('update')->setWithExpire('update_project_projects', $projects, 3600);
-    }
+    $include_disabled = config('update.settings')->get('check.disabled_extensions');
+    $projects = array_filter(project_list(), function ($project) use ($include_disabled) {
+      return ($include_disabled || !empty($project['project_status'])) &&
+        (!isset($project['info']['project status url']) || $project['info']['project status url']);
+    });
   }
   return $projects;
 }
 
 /**
- * Populates an array of project data.
- *
- * This iterates over a list of the installed modules or themes and groups
- * them by project and status. A few parts of this function assume that
- * enabled modules and themes are always processed first, and if disabled
- * modules or themes are being processed (there is a setting to control if
- * disabled code should be included in the Available updates report or not),
- * those are only processed after $projects has been populated with
- * information about the enabled code. 'Hidden' modules are always ignored.
- * 'Hidden' themes are ignored only if they have no enabled sub-themes.
- * This function also records the latest change time on the .info.yml
- * files for each module or theme, which is important data which is used when
- * deciding if the available update data should be invalidated.
- *
- * @param $projects
- *   Reference to the array of project data of what's installed on this site.
- * @param $list
- *   Array of data to process to add the relevant info to the $projects array.
- * @param $project_type
- *   The kind of data in the list. Can be 'module' or 'theme'.
- * @param $status
- *   Boolean that controls what status (enabled or disabled) to process out of
- *   the $list and add to the $projects array.
- * @param $additional_whitelist
- *   (optional) Array of additional elements to be collected from the .info.yml
- *   file. Defaults to array().
- *
- * @see update_get_projects()
- */
-function update_process_info_list(&$projects, $list, $project_type, $status, $additional_whitelist = array()) {
-  foreach ($list as $file) {
-    // A disabled or hidden base theme of an enabled sub-theme still has all
-    // of its code run by the sub-theme, so we include it in our "enabled"
-    // projects list.
-    if ($status && !empty($file->sub_themes)) {
-      foreach ($file->sub_themes as $key => $name) {
-        // Build a list of enabled sub-themes.
-        if ($list[$key]->status) {
-          $file->enabled_sub_themes[$key] = $name;
-        }
-      }
-      // If the theme is disabled and there are no enabled subthemes, we
-      // should ignore this base theme for the enabled case. If the site is
-      // trying to display disabled themes, we'll catch it then.
-      if (!$file->status && empty($file->enabled_sub_themes)) {
-        continue;
-      }
-    }
-    // Otherwise, just add projects of the proper status to our list.
-    elseif ($file->status != $status) {
-      continue;
-    }
-
-    // Skip if the .info.yml file is broken.
-    if (empty($file->info)) {
-      continue;
-    }
-
-    // Skip if it's a hidden module or hidden theme without enabled sub-themes.
-    if (!empty($file->info['hidden']) && empty($file->enabled_sub_themes)) {
-      continue;
-    }
-
-    // If the .info.yml doesn't define the 'project', try to figure it out.
-    if (!isset($file->info['project'])) {
-      $file->info['project'] = update_get_project_name($file);
-    }
-
-    // If we still don't know the 'project', give up.
-    if (empty($file->info['project'])) {
-      continue;
-    }
-
-    // If we don't already know it, grab the change time on the .info.yml file
-    // itself. Note: we need to use the ctime, not the mtime (modification
-    // time) since many (all?) tar implementations will go out of their way to
-    // set the mtime on the files it creates to the timestamps recorded in the
-    // tarball. We want to see the last time the file was changed on disk,
-    // which is left alone by tar and correctly set to the time the .info.yml
-    // file was unpacked.
-    if (!isset($file->info['_info_file_ctime'])) {
-      $info_filename = dirname($file->uri) . '/' . $file->name . '.info.yml';
-      $file->info['_info_file_ctime'] = filectime($info_filename);
-    }
-
-    if (!isset($file->info['datestamp'])) {
-      $file->info['datestamp'] = 0;
-    }
-
-    $project_name = $file->info['project'];
-
-    // Figure out what project type we're going to use to display this module
-    // or theme. If the project name is 'drupal', we don't want it to show up
-    // under the usual "Modules" section, we put it at a special "Drupal Core"
-    // section at the top of the report.
-    if ($project_name == 'drupal') {
-      $project_display_type = 'core';
-    }
-    else {
-      $project_display_type = $project_type;
-    }
-    if (empty($status) && empty($file->enabled_sub_themes)) {
-      // If we're processing disabled modules or themes, append a suffix.
-      // However, we don't do this to a a base theme with enabled
-      // subthemes, since we treat that case as if it is enabled.
-      $project_display_type .= '-disabled';
-    }
-    // Add a list of sub-themes that "depend on" the project and a list of base
-    // themes that are "required by" the project.
-    if ($project_name == 'drupal') {
-      // Drupal core is always required, so this extra info would be noise.
-      $sub_themes = array();
-      $base_themes = array();
-    }
-    else {
-      // Add list of enabled sub-themes.
-      $sub_themes = !empty($file->enabled_sub_themes) ? $file->enabled_sub_themes : array();
-      // Add list of base themes.
-      $base_themes = !empty($file->base_themes) ? $file->base_themes : array();
-    }
-    if (!isset($projects[$project_name])) {
-      // Only process this if we haven't done this project, since a single
-      // project can have multiple modules or themes.
-      $projects[$project_name] = array(
-        'name' => $project_name,
-        // Only save attributes from the .info.yml file we care about so we do
-        // not bloat our RAM usage needlessly.
-        'info' => update_filter_project_info($file->info, $additional_whitelist),
-        'datestamp' => $file->info['datestamp'],
-        'includes' => array($file->name => $file->info['name']),
-        'project_type' => $project_display_type,
-        'project_status' => $status,
-        'sub_themes' => $sub_themes,
-        'base_themes' => $base_themes,
-      );
-    }
-    elseif ($projects[$project_name]['project_type'] == $project_display_type) {
-      // Only add the file we're processing to the 'includes' array for this
-      // project if it is of the same type and status (which is encoded in the
-      // $project_display_type). This prevents listing all the disabled
-      // modules included with an enabled project if we happen to be checking
-      // for disabled modules, too.
-      $projects[$project_name]['includes'][$file->name] = $file->info['name'];
-      $projects[$project_name]['info']['_info_file_ctime'] = max($projects[$project_name]['info']['_info_file_ctime'], $file->info['_info_file_ctime']);
-      $projects[$project_name]['datestamp'] = max($projects[$project_name]['datestamp'], $file->info['datestamp']);
-      if (!empty($sub_themes)) {
-        $projects[$project_name]['sub_themes'] += $sub_themes;
-      }
-      if (!empty($base_themes)) {
-        $projects[$project_name]['base_themes'] += $base_themes;
-      }
-    }
-    elseif (empty($status)) {
-      // If we have a project_name that matches, but the project_display_type
-      // does not, it means we're processing a disabled module or theme that
-      // belongs to a project that has some enabled code. In this case, we add
-      // the disabled thing into a separate array for separate display.
-      $projects[$project_name]['disabled'][$file->name] = $file->info['name'];
-    }
-  }
-}
-
-/**
- * Determines what project a given file object belongs to.
- *
- * @param $file
- *   A file object as returned by system_get_files_database().
- *
- * @return
- *   The canonical project short name.
- *
- * @see system_get_files_database()
- */
-function update_get_project_name($file) {
-  $project_name = '';
-  if (isset($file->info['project'])) {
-    $project_name = $file->info['project'];
-  }
-  elseif (isset($file->filename) && (strpos($file->filename, 'core/modules') === 0)) {
-    $project_name = 'drupal';
-  }
-  return $project_name;
-}
-
-/**
- * Determines version and type information for currently installed projects.
- *
- * Processes the list of projects on the system to figure out the currently
- * installed versions, and other information that is required before we can
- * compare against the available releases to produce the status report.
- *
- * @param $projects
- *   Array of project information from update_get_projects().
- */
-function update_process_project_info(&$projects) {
-  foreach ($projects as $key => $project) {
-    // Assume an official release until we see otherwise.
-    $install_type = 'official';
-
-    $info = $project['info'];
-
-    if (isset($info['version'])) {
-      // Check for development snapshots
-      if (preg_match('@(dev|HEAD)@', $info['version'])) {
-        $install_type = 'dev';
-      }
-
-      // Figure out what the currently installed major version is. We need
-      // to handle both contribution (e.g. "5.x-1.3", major = 1) and core
-      // (e.g. "5.1", major = 5) version strings.
-      $matches = array();
-      if (preg_match('/^(\d+\.x-)?(\d+)\..*$/', $info['version'], $matches)) {
-        $info['major'] = $matches[2];
-      }
-      elseif (!isset($info['major'])) {
-        // This would only happen for version strings that don't follow the
-        // drupal.org convention. We let contribs define "major" in their
-        // .info.yml in this case, and only if that's missing would we hit this.
-        $info['major'] = -1;
-      }
-    }
-    else {
-      // No version info available at all.
-      $install_type = 'unknown';
-      $info['version'] = t('Unknown');
-      $info['major'] = -1;
-    }
-
-    // Finally, save the results we care about into the $projects array.
-    $projects[$key]['existing_version'] = $info['version'];
-    $projects[$key]['existing_major'] = $info['major'];
-    $projects[$key]['install_type'] = $install_type;
-  }
-}
-
-/**
  * Calculates the current update status of all projects on the site.
  *
  * The results of this function are expensive to compute, especially on sites
@@ -332,8 +54,8 @@ function update_process_project_info(&$projects) {
  *
  * @see update_get_available()
  * @see update_get_projects()
- * @see update_process_project_info()
- * @see update_project_storage()
+ * @see project_process_version_info()
+ * @see update_project_cache()
  */
 function update_calculate_project_data($available) {
   // Retrieve the projects from storage, if present.
@@ -344,7 +66,7 @@ function update_calculate_project_data($available) {
     return $projects;
   }
   $projects = update_get_projects();
-  update_process_project_info($projects);
+  project_process_version_info($projects);
   foreach ($projects as $project => $project_info) {
     if (isset($available[$project])) {
       update_calculate_project_update_status($projects[$project], $available[$project]);
@@ -800,32 +522,3 @@ function update_project_storage($key) {
   }
   return $projects;
 }
-
-/**
- * Filters the project .info.yml data to only save attributes we need.
- *
- * @param array $info
- *   Array of .info.yml file data as returned by drupal_parse_info_file().
- * @param $additional_whitelist
- *   (optional) Array of additional elements to be collected from the .info.yml
- *   file. Defaults to array().
- *
- * @return
- *   Array of .info.yml file data we need for the update manager.
- *
- * @see update_process_info_list()
- */
-function update_filter_project_info($info, $additional_whitelist = array()) {
-  $whitelist = array(
-    '_info_file_ctime',
-    'datestamp',
-    'major',
-    'name',
-    'package',
-    'project',
-    'project status url',
-    'version',
-  );
-  $whitelist = array_merge($whitelist, $additional_whitelist);
-  return array_intersect_key($info, drupal_map_assoc($whitelist));
-}
