diff --git a/core/lib/Drupal/Core/Utility/ProjectInfo.php b/core/lib/Drupal/Core/Utility/ProjectInfo.php
new file mode 100644
index 0000000..f6bbe34
--- /dev/null
+++ b/core/lib/Drupal/Core/Utility/ProjectInfo.php
@@ -0,0 +1,229 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Utility\ProjectInfo.
+ *
+ * API for building lists of installed projects.
+ */
+
+namespace Drupal\Core\Utility;
+
+/**
+ * Performs operations on drupal.org project data.
+ */
+class ProjectInfo {
+
+  /**
+   * 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().
+   */
+  function processInfoList(&$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'] = $this->getProjectName($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' => $this->filterProjectInfo($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 getProjectName($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.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 \Drupal\Core\Utility\ProjectInfo->processInfoList()
+   */
+  function filterProjectInfo($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));
+  }
+
+}
diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc
index 3cbaac5..d0271bd 100644
--- a/core/modules/locale/locale.compare.inc
+++ b/core/modules/locale/locale.compare.inc
@@ -6,9 +6,10 @@
  */
 
 use Drupal\Core\Cache;
+use Drupal\Core\Utility\ProjectInfo;
 
 /**
- * Load the common translation API.
+ * Load common APIs.
  */
 // @todo Combine functions differently in files to avoid unnecessary includes.
 // Follow-up issue http://drupal.org/node/1834298
@@ -126,11 +127,6 @@ 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 (!Drupal::moduleHandler()->moduleExists('update')) {
-    return array();
-  }
-
   $projects = &drupal_static(__FUNCTION__, array());
   if (empty($projects)) {
     module_load_include('compare.inc', 'update');
@@ -143,11 +139,12 @@ function locale_translation_project_list() {
     );
     $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);
+    $project_info = new ProjectInfo();
+    $project_info->processInfoList($projects, $module_data, 'module', TRUE, $additional_whitelist);
+    $project_info->processInfoList($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);
+      $project_info->processInfoList($projects, $module_data, 'module', FALSE, $additional_whitelist);
+      $project_info->processInfoList($projects, $theme_data, 'theme', FALSE, $additional_whitelist);
     }
 
     // Allow other modules to alter projects before fetching and comparing.
@@ -159,9 +156,10 @@ function locale_translation_project_list() {
 /**
  * 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.
+ * Modify .info.yml file data before it is processed by
+ * \Drupal\Core\Utility\ProjectInfo->processInfoList(). In order for
+ * \Drupal\Core\Utility\ProjectInfo->processInfoList() 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
@@ -179,8 +177,9 @@ function locale_translation_project_list() {
 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.
+    // \Drupal\Core\Utility\ProjectInfo->processInfoList() 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'];
     }
diff --git a/core/modules/update/lib/Drupal/update/Tests/UpdateContribTest.php b/core/modules/update/lib/Drupal/update/Tests/UpdateContribTest.php
index 1fdd0b3..757894f 100644
--- a/core/modules/update/lib/Drupal/update/Tests/UpdateContribTest.php
+++ b/core/modules/update/lib/Drupal/update/Tests/UpdateContribTest.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\update\Tests;
 
+use Drupal\Core\Utility\ProjectInfo;
+
 /**
  * Tests behavior related to handling updates to contributed modules and themes.
  */
@@ -287,7 +289,8 @@ function testUpdateHiddenBaseTheme() {
     config('update_test.settings')->set('system_info', $system_info)->save();
     $projects = update_get_projects();
     $theme_data = system_rebuild_theme_data();
-    update_process_info_list($projects, $theme_data, 'theme', TRUE);
+    $project_info = new ProjectInfo();
+    $project_info->processInfoList($projects, $theme_data, 'theme', TRUE);
 
     $this->assertTrue(!empty($projects['update_test_basetheme']), 'Valid base theme (update_test_basetheme) was found.');
   }
diff --git a/core/modules/update/update.api.php b/core/modules/update/update.api.php
index b5621dd..9e5f937 100644
--- a/core/modules/update/update.api.php
+++ b/core/modules/update/update.api.php
@@ -28,11 +28,11 @@
  *   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.
+ *   \Drupal\Core\Utility\ProjectInfo->processInfoList(), so look there for
+ *   examples of how to populate the array with real values.
  *
  * @see update_get_projects()
- * @see update_process_info_list()
+ * @see \Drupal\Core\Utility\ProjectInfo->processInfoList()
  */
 function hook_update_projects_alter(&$projects) {
   // Hide a site-specific module from the list.
diff --git a/core/modules/update/update.compare.inc b/core/modules/update/update.compare.inc
index c8f5958..6741b41 100644
--- a/core/modules/update/update.compare.inc
+++ b/core/modules/update/update.compare.inc
@@ -5,6 +5,8 @@
  * Code required only when comparing available updates to existing data.
  */
 
+use Drupal\Core\Utility\ProjectInfo;
+
 /**
  * Fetches an array of installed and enabled projects.
  *
@@ -61,11 +63,12 @@ function update_get_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);
+      $project_info = new ProjectInfo();
+      $project_info->processInfoList($projects, $module_data, 'module', TRUE);
+      $project_info->processInfoList($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);
+        $project_info->processInfoList($projects, $module_data, 'module', FALSE);
+        $project_info->processInfoList($projects, $theme_data, 'theme', FALSE);
       }
       // Allow other modules to alter projects before fetching and comparing.
       drupal_alter('update_projects', $projects);
@@ -77,191 +80,6 @@ function update_get_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
@@ -800,32 +618,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));
-}
