diff --git a/core/includes/module.inc b/core/includes/module.inc
index 11f8244..16a0387 100644
--- a/core/includes/module.inc
+++ b/core/includes/module.inc
@@ -115,6 +115,7 @@ function system_list_reset() {
   drupal_static_reset('list_themes');
   cache('bootstrap')->delete('system_list');
   cache()->delete('system_info');
+  cache()->delete('system_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..0fe676d
--- /dev/null
+++ b/core/includes/project.inc
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @file
+ * API for building lists of installed projects.
+ */
+
+use Drupal\Core\Project\ProjectBuilder;
+use Drupal\Core\Project\ProjectFactory;
+
+/**
+ * Fetches an array of installed and enabled projects.
+ *
+ * @todo Review this documentation for changes from array to object and array
+ *   properties versus getters/setters.
+ *
+ * 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 array
+ *   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 system_project_list() {
+  if ($cached = cache()->get('system_project_list')) {
+    $projects = $cached->data;
+  }
+  else {
+    $module_data = system_rebuild_module_data();
+    $theme_data = system_rebuild_theme_data();
+    $builder = new ProjectBuilder($module_data, $theme_data, new ProjectFactory());
+    $projects = $builder->getAllProjects();
+
+    // Allow other modules to alter projects before fetching and comparing.
+    drupal_alter('system_project_list', $projects);
+    // Cache the site's project data for at most 1 hour.
+    cache()->set('system_project_list', $projects, REQUEST_TIME + 3600);
+  }
+  return $projects;
+}
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/lib/Drupal/Core/Project/Project.php b/core/lib/Drupal/Core/Project/Project.php
new file mode 100644
index 0000000..14c1367
--- /dev/null
+++ b/core/lib/Drupal/Core/Project/Project.php
@@ -0,0 +1,284 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Project\Project.
+ */
+
+namespace Drupal\Core\Project;
+
+/**
+ * @todo
+ */
+class Project {
+
+  /**
+   * The machine readable project name.
+   *
+   * @var string
+   */
+  protected $name = '';
+
+  /**
+   * The type of project. e.g. module, theme, core, etc.
+   *
+   * @var string
+   */
+  protected $type = '';
+
+  /**
+   * The project status. TRUE = enabled, FALSE = disabled.
+   *
+   * @var string
+   */
+  protected $status = '';
+
+  /**
+   * Array of project info, as defined in the .info file.
+   *
+   * @var array
+   */
+  protected $info = array();
+
+  /**
+   * The timestamp of project release.
+   *
+   * @var int
+   */
+  protected $datestamp = 0;
+
+  /**
+   * Array of modules included in this project.
+   *
+   * @var array
+   */
+  protected $includes = array();
+
+  /**
+   * Array of disabled modules in this project.
+   *
+   * @var array
+   */
+  protected $disabled = array();
+
+  /**
+   * Array of base themes required by the project.
+   *
+   * @var array
+   */
+  protected $baseThemes = array();
+
+  /**
+   * Array of sub themes depending on this project.
+   *
+   * @var array
+   */
+  protected $subThemes = array();
+
+  /**
+   * @todo
+   */
+  public function __construct($name, $type, $status, $info) {
+    $this->name = $name;
+    $this->type = $type;
+    $this->status = (boolean) $status;
+    $this->info = $info;
+    $this->datestamp = isset($info['datestamp']) ? $info['datestamp'] : 0;
+    $this->setIncludes($name, $info['name']);
+  }
+
+  /**
+   * Sets the project datestamp to the maximum value.
+   *
+   * @param int $datestamp
+   *   Datestamp the module or theme was created. Only the most recent date
+   *   stamp of the included modules or themes will be stored.
+   */
+  public function setDatestamp($datestamp) {
+    $this->datestamp = max($this->datestamp, $datestamp);
+  }
+
+  /**
+   * Sets the info file change time to the maximum timestamp value.
+   *
+   * @param int $timestamp
+   *   Timestamp when the module's or theme's .info file was created. Only the
+   *   most recent time stamp of the included modules or themes will be stored.
+   */
+  public function setChangeTime($timestamp) {
+    $this->info['_info_file_ctime'] = max($this->info['_info_file_ctime'], $timestamp);
+  }
+
+  /**
+   * Adds inluded modules or themes to the project.
+   *
+   * @param string $name
+   *   Machine readable module or theme name.
+   * @param string $title
+   *   Human readable module or theme name.
+   */
+  public function setIncludes($name, $title) {
+    $this->includes[$name] = $title;
+  }
+
+  /**
+   * Adds disabled modules or themes to the project.
+   *
+   * @param string $name
+   *   Machine readable module or theme name.
+   * @param string $title
+   *   Human readable module or theme name.
+   */
+  public function setDisabled($name, $title) {
+    $this->disabled[$name] = $title;
+  }
+
+  /**
+   * Adds base themes related to this project.
+   *
+   * @param array $themes
+   *   Array of related base themes keyed by machine readable theme name.
+   */
+  public function setBaseThemes($themes) {
+    if (!empty($themes) && is_array($themes)) {
+      $this->baseThemes += $themes;
+    }
+  }
+
+  /**
+   * Adds the sub theme related to this project.
+   *
+   * @param array $themes
+   *   Array of related sub themes keyed by machine readable theme name.
+   */
+  public function setSubThemes($themes) {
+    if (!empty($themes) && is_array($themes)) {
+      $this->subThemes += $themes;
+    }
+  }
+
+  /**
+   * Adds project info data.
+   *
+   * @param array $info
+   *   Associative array of project info data.
+   */
+  public function setInfo($info) {
+    if (!empty($info) && is_array($info)) {
+      $this->info += $info;
+    }
+  }
+
+  /**
+   * Gets the machine readable name of the project.
+   *
+   * @return string
+   *   Machine readable name of the project.
+   */
+  public function getName() {
+    return $this->name;
+  }
+
+  /**
+   * Gets the project type.
+   *
+   * @return string
+   *   Project type. e.g. 'module', 'theme', 'core'.
+   */
+  public function getType() {
+    return $this->type;
+  }
+
+  /**
+   * Gets the project status.
+   *
+   * @return bool
+   *   TRUE: the project is enabled. FALSE if disabled.
+   */
+  public function getStatus() {
+    return $this->status;
+  }
+
+  /**
+   * Gets the timestamp of project release.
+   *
+   * @return int
+   *   Timestamp of project release. Zero if not set.
+   */
+  public function getDatestamp() {
+    return $this->datestamp;
+  }
+
+  /**
+   * Gets the modules included in this project.
+   *
+   * @return array
+   *   Modules included in this project, keyed by machine readable module name.
+   */
+  public function getIncludes() {
+    return $this->includes;
+  }
+
+  /**
+   * Gets the disabled modules in this project.
+   *
+   * @return array
+   *   Disabled modules in this project, keyed by machine readable module name.
+   */
+  public function getDisabled() {
+    return $this->disabled;
+  }
+
+  /**
+   * Gets the base themes required by this project.
+   *
+   * @return array
+   *   Related base themes, keyed by machine readable theme name.
+   */
+  public function getBaseThemes() {
+    return $this->baseThemes;
+  }
+
+  /**
+   * Gets the sub themes depending on this project.
+   *
+   * @return array
+   *   Related sub themes, keyed by machine readable theme name.
+   */
+  public function getSubThemes() {
+    return $this->subThemes;
+  }
+
+  /**
+   * Gets the raw project info data.
+   *
+   * @return array
+   *   Project info data.
+   */
+  public function getInfoRaw() {
+    return $this->info;
+  }
+
+  /**
+   * Gets specific project info data.
+   *
+   * @param string $param
+   *   The name of the project info data to return.
+   *
+   * @return string
+   *   The specified project info data or NULL is not found.
+   */
+  public function getInfo($param) {
+    return isset($this->info[$param]) ? $this->info[$param] : NULL;
+  }
+
+  /**
+   * Gets human readable project name.
+   *
+   * @return string
+   *   Human readable project name. Falls back to machine name if not available.
+   */
+  public function getProjectName() {
+    return isset($this->info['name']) ? $this->project->info['name'] : $this->name;
+  }
+}
diff --git a/core/lib/Drupal/Core/Project/ProjectBuilder.php b/core/lib/Drupal/Core/Project/ProjectBuilder.php
new file mode 100644
index 0000000..db3b130
--- /dev/null
+++ b/core/lib/Drupal/Core/Project/ProjectBuilder.php
@@ -0,0 +1,283 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Project\ProjectBuilder.
+ */
+
+namespace Drupal\Core\Project;
+
+use Drupal\Core\Project\Project;
+
+/**
+ * @todo
+ */
+class ProjectBuilder {
+
+  /**
+   * Array of Project objects.
+   *
+   * @var array
+   */
+  protected $projects = array();
+
+  /**
+   * The object that instantiates projects build by this project builder.
+   *
+   * @var Drupal\Core\Project\ProjectFactory.
+   */
+  protected $factory;
+
+  /**
+   * @todo Finish documentation
+   *
+   * @param array $module_data
+   * @apram array $theme_data
+   * @param Drupal\Core\Project\ProjectFactory $factory
+   *   Name of the class used to create the individual project objects.
+   */
+  // @todo Consider using a $project_class instead of $factory.
+  public function __construct($module_data, $theme_data, $factory) {
+    $this->factory = $factory;
+
+    // First parse data of enabled modules (or themes) only then parse data of
+    // the disabled ones.
+    $this->parse($module_data, 'module', TRUE);
+    $this->parse($module_data, 'module', FALSE);
+    $this->parse($theme_data, 'theme', TRUE);
+    $this->parse($theme_data, 'theme', FALSE);
+  }
+
+  /**
+   * Returns all build projects.
+   *
+   * @return array
+   *   Projects keyed by the machine readable project name.
+   */
+  public function getAllProjects() {
+    return $this->projects;
+  }
+
+  /**
+   * Returns a single project object.
+   *
+   * @return Drupal\Core\Project\Project
+   *   The selected project or NULL if not found.
+   */
+  public function getProject($name) {
+    if (isset($this->projects[$name])) {
+      return $this->projects[$name];
+    }
+  }
+
+  /**
+   * Stores a project in the internal project array.
+   *
+   * @param object $project
+   *   Project object.
+   */
+  protected function setProject($project) {
+    $this->projects[$project->getName()] = $project;
+  }
+
+  /**
+   * Parse project list and retrieve 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 array $list
+   *   Data to process to add the relevant info to the $projects array.
+   * @param string $project_type
+   *   The kind of data in the list. Can be 'module' or 'theme'.
+   * @param bool $status
+   *   The status (enabled or disabled) to process out of the $list and add to
+   *   the $projects array.
+   */
+  public function parse($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'] = $this->fileGetName($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 (!$project = $this->getProject($project_name)) {
+        // Only process this if we haven't done this project, since a single
+        // project can have multiple modules or themes.
+        // @todo dynamically define a class.
+
+        // Only save attributes from the .info file we care about so we do not
+        // bloat our RAM usage needlessly.
+        $project = $this->factory->createInstance($project_name, $project_display_type, $status, $this->filterInfo($file->info));
+        $project->setSubThemes($sub_themes);
+        $project->setBaseThemes($base_themes);
+      }
+      elseif ($project->getType() == $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.
+        $project->setIncludes($file->name, $file->info['name']);
+        $project->setChangeTime($file->info['_info_file_ctime']);
+        $project->setDatestamp($file->info['datestamp']);
+        $project->setSubThemes($sub_themes);
+        $project->setBaseThemes($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.
+        $project->setDisabled($file->name, $file->info['name']);
+      }
+
+      // Store the project.
+      if (!empty($project)) {
+        $this->setProject($project);
+      }
+    }
+  }
+
+  /**
+   * Determines what project a given file object belongs to.
+   *
+   * @param object $file
+   *   A file object as returned by system_get_files_database().
+   *
+   * @return string
+   *   The canonical project short name.
+   */
+  protected function fileGetName($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;
+  }
+
+  /**
+   * 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
+   *   Array of .info file data we need for the project list.
+   */
+  protected function filterInfo($info) {
+    $return = array();
+    $whitelist = array(
+      '_info_file_ctime',
+      'datestamp',
+      'major',
+      'name',
+      'package',
+      'version',
+    );
+
+    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/Core/Project/ProjectFactory.php b/core/lib/Drupal/Core/Project/ProjectFactory.php
new file mode 100644
index 0000000..b0d6470
--- /dev/null
+++ b/core/lib/Drupal/Core/Project/ProjectFactory.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Project\ProjectFactory.
+ */
+
+namespace Drupal\Core\Project;
+
+class ProjectFactory {
+
+  /**
+   * @todo
+   */
+  public function createInstance($name, $type, $status, $info) {
+    return new Project($name, $type, $status, $info);
+  }
+}
diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php
index e5dccbd..fd5eed9 100644
--- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php
+++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleUpdateTest.php
@@ -183,9 +183,11 @@ private function makePoFile($path, $filename, $timestamp = NULL, $translations =
   private function setTranslationFiles() {
     $config = config('locale.settings');
 
-    // A flag is set to let the locale_test module replace the project data with
-    // a set of test projects which match the below project files.
+    // Set a flag to let the locale_test module replace the project data with
+    // a set of test projects which match the below project files. Clear the
+    // project list data to contain the new definition at the next request.
     state()->set('locale.test_projects_alter', TRUE);
+    cache()->delete('system_project_list');
 
     // Setup the environment.
     $public_path = variable_get('file_public_path', conf_path() . '/files');
@@ -320,7 +322,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')));
   }
 
@@ -352,8 +354,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_system_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
@@ -362,9 +364,6 @@ function testUpdateProjectsHidden() {
    */
   function testUpdateCheckStatus() {
     $config = config('locale.settings');
-    // Set a flag to let the locale_test module replace the project data with a
-    // set of test projects.
-    state()->set('locale.test_projects_alter', TRUE);
 
     // Create local and remote translations files.
     $this->setTranslationFiles();
diff --git a/core/modules/locale/locale.api.php b/core/modules/locale/locale.api.php
index 0db2091..a3dd9cc 100644
--- a/core/modules/locale/locale.api.php
+++ b/core/modules/locale/locale.api.php
@@ -29,7 +29,7 @@
  * 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 +37,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_system_project_list_alter(). Possible changes
  * include changing the po file location (server pattern) or removing the
  * project from the translation update list.
  *
@@ -67,7 +67,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 +80,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 9eba52e..9505c85 100644
--- a/core/modules/locale/locale.compare.inc
+++ b/core/modules/locale/locale.compare.inc
@@ -6,6 +6,7 @@
  */
 
 use Drupal\Core\Cache;
+use Drupal\Core\Project\LocaleProjectBuilder;
 
 /**
  * Load the common translation API.
@@ -45,10 +46,6 @@ 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 (!module_exists('update')) {
-    return array();
-  }
 
   // Get the project list based on .info.yml files.
   $projects = locale_translation_project_list();
@@ -63,7 +60,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.
@@ -74,7 +71,7 @@ function locale_translation_build_projects() {
           // dev release is the one. Releases are sorted the most recent first.
           // @todo http://drupal.org/node/1774024 Make a helper function.
           if ($project_release['version_major'] == $matches[1] &&
-              (!isset($project_release['version_extra']) || $project_release['version_extra'] != 'dev')) {
+            (!isset($project_release['version_extra']) || $project_release['version_extra'] != 'dev')) {
             $release = $project_release;
             break;
           }
@@ -99,7 +96,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;
@@ -118,8 +115,8 @@ function locale_translation_build_projects() {
       ))
       ->execute();
 
-      // Invalidate the cache of translatable projects.
-      locale_translation_clear_cache_projects();
+    // Invalidate the cache of translatable projects.
+    locale_translation_clear_cache_projects();
   }
   return $projects;
 }
@@ -131,67 +128,31 @@ function locale_translation_build_projects() {
  *   Array of project data including .info.yml file data.
  */
 function locale_translation_project_list() {
-  // This function depends on Update module. We degrade gracefully.
-  if (!module_exists('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);
+    require_once DRUPAL_ROOT . '/core/includes/project.inc';
+
+    $include_disabled = config('locale.settings')->get('translation.check_disabled_modules');
+    $projects = array_filter(system_project_list(), function ($project) use ($include_disabled) {
+      return ($include_disabled || $project->getStatus()) &&
+        ($project->getInfo('project translation server pattern') !== FALSE);
+    });
+
+    // We convert the project objects to arrays to be compatible with the
+    // functions in locale module using this project list.
+    // @todo Create a follow-up issue to fix this.
+    foreach ($projects as $key => $project) {
+      $projects[$key] = array(
+        'name' => $project->getName(),
+        'project_type' => $project->getType(),
+        'project_status' => $project->getStatus(),
+        'info' => $project->getInfoRaw(),
+      );
     }
-
-    // Allow other modules to alter projects before fetching and comparing.
-    drupal_alter('locale_translation_projects', $projects);
   }
   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.
@@ -357,7 +318,7 @@ function _locale_translation_batch_status_operations($projects, $langcodes) {
  * 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().
+ * locale_test_system_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/tests/modules/locale_test/locale_test.info.yml b/core/modules/locale/tests/modules/locale_test/locale_test.info.yml
index bd5f032..4e5f581 100644
--- a/core/modules/locale/tests/modules/locale_test/locale_test.info.yml
+++ b/core/modules/locale/tests/modules/locale_test/locale_test.info.yml
@@ -4,5 +4,7 @@ package: Testing
 version: '1.2'
 core: 8.x
 hidden: true
-'interface translation project': locale_test
-'interface translation server pattern': core/modules/locale/test/test.%language.po
+
+# Hide this project from update status.
+'project status url': '0'
+'project translation server pattern': core/modules/locale/test/test.%language.po
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 88e581c..1d6b632 100644
--- a/core/modules/locale/tests/modules/locale_test/locale_test.module
+++ b/core/modules/locale/tests/modules/locale_test/locale_test.module
@@ -5,6 +5,9 @@
  * Simulate a custom module with a local po file.
  */
 
+use Drupal\Core\Project\ProjectBuilder;
+use Drupal\Core\Project\ProjectFactory;
+
 /**
  * Implements hook_system_info_alter().
  *
@@ -25,7 +28,7 @@ function locale_test_system_info_alter(&$info, $file, $type) {
 }
 
 /**
- * Implements hook_locale_translation_projects_alter().
+ * Implements hook_system_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 +39,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_system_project_list_alter(&$projects) {
   if (state()->get('locale.test_projects_alter')) {
 
     // Instead of the default ftp.drupal.org we use the file system of the test
@@ -46,98 +49,90 @@ function locale_test_locale_translation_projects_alter(&$projects) {
 
     // Completely replace the project data with a set of test projects.
     $base_url = url();
-    $projects = array (
-      'drupal' => array (
+    $module_data = array (
+      'drupal' => (object) array (
         'name' => 'drupal',
+        'status' => 1,
         'info' => array (
           'name' => 'Drupal',
-          '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' => 'Core',
           'version' => '8.0',
           'project' => 'drupal',
           '_info_file_ctime' => 1348824632,
           'datestamp' => 0,
         ),
-        'datestamp' => 0,
-        'project_type' => 'core',
-        'project_status' => TRUE,
       ),
-      'contrib_module_one' => array (
+      'contrib_module_one' => (object) array (
         'name' => 'contrib_module_one',
         'info' => array (
           'name' => 'Contributed module one',
-          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
+          'status' => 1,
+          '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',
-        'project_status' => TRUE,
       ),
-      'contrib_module_two' => array (
+      'contrib_module_two' => (object) array (
         'name' => 'contrib_module_two',
+        'status' => 1,
         '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',
-        'project_status' => TRUE,
       ),
-      'contrib_module_three' => array (
+      'contrib_module_three' => (object) array (
         'name' => 'contrib_module_three',
+        'status' => 1,
         'info' => array (
           'name' => 'Contributed module three',
-          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
+          'status' => 1,
+          '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',
-        'project_status' => TRUE,
       ),
-      'locale_test' => array (
+      'locale_test' => (object) array (
         'name' => 'locale_test',
+        'status' => 1,
         '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',
-        'project_status' => TRUE,
       ),
-      'custom_module_one' => array (
+      'custom_module_one' => (object) array (
         'name' => 'custom_module_one',
+        'status' => 1,
         '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',
-        'project_status' => TRUE,
       ),
     );
   }
diff --git a/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.info.yml b/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.info.yml
index a212a14..d9044ba 100644
--- a/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.info.yml
+++ b/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.info.yml
@@ -4,5 +4,6 @@ package: Testing
 version: '1.3'
 core: 8.x
 hidden: true
-'interface translation project': locale_test_translate
-'interface translation server pattern': core/modules/locale/tests/test.%language.po
+
+# Definitions for interface translations.
+'project translation server pattern': core/modules/locale/tests/test.%language.po
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 83f4587..9c4953d 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -1495,6 +1495,103 @@ function hook_system_info_alter(&$info, $file, $type) {
 }
 
 /**
+ * Alter the list of projects before fetching data and comparing versions.
+ *
+ * @todo Review this documentation. $projects is now a list of objects.
+ *
+ * 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 system_project_list()
+ * @see project_process_info_list()
+ * @see update_get_projects()
+ * @see locale_translation_project_list()
+ */
+function hook_system_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/tests/modules/update_test/update_test.info.yml b/core/modules/update/tests/modules/update_test/update_test.info.yml
index b86daf3..5cd9588 100644
--- a/core/modules/update/tests/modules/update_test/update_test.info.yml
+++ b/core/modules/update/tests/modules/update_test/update_test.info.yml
@@ -3,4 +3,4 @@ description: 'Support module for update module testing.'
 package: Testing
 version: VERSION
 core: 8.x
-hidden: true
+# hidden: true
diff --git a/core/modules/update/update.api.php b/core/modules/update/update.api.php
index b5621dd..ae01200 100644
--- a/core/modules/update/update.api.php
+++ b/core/modules/update/update.api.php
@@ -11,69 +11,6 @@
  */
 
 /**
- * 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..51f52be 100644
--- a/core/modules/update/update.compare.inc
+++ b/core/modules/update/update.compare.inc
@@ -5,260 +5,49 @@
  * Code required only when comparing available updates to existing data.
  */
 
+use Drupal\Core\Project\UpdateProjectBuilder;
+
 /**
  * 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
+ * @return array
  *   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 name.
  *
- * @see update_process_project_info()
  * @see update_calculate_project_data()
  * @see update_project_storage()
  */
 function update_get_projects() {
   $projects = &drupal_static(__FUNCTION__, array());
+  // @todo Check the behaviour if 'project status url' in .info file is set at different values. Amnong which 'FALSE'.
   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);
-    }
-  }
-  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,
+    require_once DRUPAL_ROOT . '/core/includes/project.inc';
+    $include_disabled = config('update.settings')->get('check.disabled_extensions');
+    $projects = array_filter(system_project_list(), function ($project) use ($include_disabled) {
+      return ($include_disabled || $project->getStatus()) &&
+        ($project->getInfo('project status url') !== FALSE);
+    });
+
+    // We convert the project objects to arrays to be compatible with the
+    // functions in update module using this project list.
+    // @todo Create a follow-up issue to fix this.
+    foreach ($projects as $key => $project) {
+      $projects[$key] = array(
+        'name' => $project->getName(),
+        'project_type' => $project->getType(),
+        'project_status' => $project->getStatus(),
+        'datestamp' => $project->getDatestamp(),
+        'info' => $project->getInfoRaw(),
+        'includes' => $project->getIncludes(),
+        'base_themes' => $project->getBaseThemes(),
+        'sub_themes' => $project->getSubThemes(),
       );
     }
-    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 $projects;
   }
-  return $project_name;
 }
 
 /**
@@ -327,7 +116,7 @@ function update_process_project_info(&$projects) {
  * @param array $available
  *   Data about available project releases.
  *
- * @return
+ * @return array
  *   An array of installed projects with current update status information.
  *
  * @see update_get_available()
@@ -555,7 +344,7 @@ function update_calculate_project_update_status(&$project_data, $available) {
     // First, if this is the existing release, check a few conditions.
     if ($project_data['existing_version'] === $version) {
       if (isset($release['terms']['Release type']) &&
-          in_array('Insecure', $release['terms']['Release type'])) {
+        in_array('Insecure', $release['terms']['Release type'])) {
         $project_data['status'] = UPDATE_NOT_SECURE;
       }
       elseif ($release['status'] == 'unpublished') {
@@ -570,7 +359,7 @@ function update_calculate_project_update_status(&$project_data, $available) {
         );
       }
       elseif (isset($release['terms']['Release type']) &&
-              in_array('Unsupported', $release['terms']['Release type'])) {
+        in_array('Unsupported', $release['terms']['Release type'])) {
         $project_data['status'] = UPDATE_NOT_SUPPORTED;
         if (empty($project_data['extra'])) {
           $project_data['extra'] = array();
@@ -585,8 +374,8 @@ function update_calculate_project_update_status(&$project_data, $available) {
 
     // Otherwise, ignore unpublished, insecure, or unsupported releases.
     if ($release['status'] == 'unpublished' ||
-        (isset($release['terms']['Release type']) &&
-         (in_array('Insecure', $release['terms']['Release type']) ||
+      (isset($release['terms']['Release type']) &&
+        (in_array('Insecure', $release['terms']['Release type']) ||
           in_array('Unsupported', $release['terms']['Release type'])))) {
       continue;
     }
@@ -619,16 +408,16 @@ function update_calculate_project_update_status(&$project_data, $available) {
     // Look for the 'latest version' if we haven't found it yet. Latest is
     // defined as the most recent version for the target major version.
     if (!isset($project_data['latest_version'])
-        && $release['version_major'] == $target_major) {
+      && $release['version_major'] == $target_major) {
       $project_data['latest_version'] = $version;
       $project_data['releases'][$version] = $release;
     }
 
     // Look for the development snapshot release for this branch.
     if (!isset($project_data['dev_version'])
-        && $release['version_major'] == $target_major
-        && isset($release['version_extra'])
-        && $release['version_extra'] == 'dev') {
+      && $release['version_major'] == $target_major
+      && isset($release['version_extra'])
+      && $release['version_extra'] == 'dev') {
       $project_data['dev_version'] = $version;
       $project_data['releases'][$version] = $release;
     }
@@ -636,8 +425,8 @@ function update_calculate_project_update_status(&$project_data, $available) {
     // Look for the 'recommended' version if we haven't found it yet (see
     // phpdoc at the top of this function for the definition).
     if (!isset($project_data['recommended'])
-        && $release['version_major'] == $target_major
-        && isset($release['version_patch'])) {
+      && $release['version_major'] == $target_major
+      && isset($release['version_patch'])) {
       if ($patch != $release['version_patch']) {
         $patch = $release['version_patch'];
         $release_patch_changed = $release;
@@ -672,7 +461,7 @@ function update_calculate_project_update_status(&$project_data, $available) {
 
     // See if this release is a security update.
     if (isset($release['terms']['Release type'])
-        && in_array('Security update', $release['terms']['Release type'])) {
+      && in_array('Security update', $release['terms']['Release type'])) {
       $project_data['security updates'][] = $release;
     }
   }
@@ -800,32 +589,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));
-}
