diff --git a/core/modules/locale/tests/modules/locale_test_mock/README.txt b/core/modules/locale/tests/modules/locale_test_mock/README.txt
new file mode 100644
index 0000000000..346b5e9f4a
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/README.txt
@@ -0,0 +1,65 @@
+Summary
+-------
+
+This module provides mock projects and mock translations for testing interface
+translation. It serves as a repository of projects and translations that can be
+enabled when needed.
+
+Enable mocking:
+  \Drupal::state()->set('locale_test_mock.use_mock_projects', TRUE);
+
+Disable interface translation update for real modules:
+  \Drupal::state()->set('locale_test_mock.hide_non_mock_projects', TRUE);
+
+Projects
+--------
+
+Mock projects are defined in a locale_test_mock.locale_mock_projects.yml. Any
+module can add projects by defining them in a MODULE.locale_mock_projects.yml
+file. Project definitions contain references to local and/or remote translation
+files.
+
+List all projects:
+  $mockManager = \Drupal::service('plugin.manager.locale_mock_project');
+  $projects = $mockManager->getProjectNames();
+
+Enable a project:
+  $mockManager = \Drupal::service('plugin.manager.locale_mock_project');
+  $mockManager->enableMockProject('contrib_module_one');
+
+Load a project:
+  $mockManager = \Drupal::service('plugin.manager.locale_mock_project');
+  $project = $mockManager->getMockProject('contrib_module_one');
+
+Local translations
+------------------
+
+Mock local translations are stored in mock_data/local_translations or any other
+placed defined by the project. A mock local file can only exist in the context
+of a mock project, not independently.
+
+Local translation files are by default located in the translations:// directory.
+Alternative file locations can be defined in the (mock) project definition using
+the 'interface translation server pattern' key.
+When the mock file is located in translations://, or public:// the mock file is
+copied from mock_data/local_translations (or other defined source location) to
+its destination. The timestamp of the file will now be set.
+
+If 'interface translation server pattern' is a drupal path (e.g.
+modules/custom/example/translations/example.po) the mock source file is _not_
+copied and not used for mocking. The timestamp of the file will not be set.
+
+Enable a local translation:
+  $project = $mockManager->getMockProject('contrib_module_one');
+  $hash = $project->enableLocalTranslation('nl', 123467890);
+
+Remote translations
+-------------------
+
+Mock remote translations are served by the Drupal site over http(s). The content
+of the mock file is returned. When the remote mock file is enabled, a timestamp
+is set which is use as 'Last-Modified' response header.
+
+Enable a remote translation:
+  $project = $mockManager->getMockProject('contrib_module_one');
+  $project->enableRemoteTranslation('nl', 123467890);
diff --git a/core/modules/locale/tests/modules/locale_test_mock/locale_test_mock.info.yml b/core/modules/locale/tests/modules/locale_test_mock/locale_test_mock.info.yml
new file mode 100644
index 0000000000..368d572a20
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/locale_test_mock.info.yml
@@ -0,0 +1,5 @@
+name: Locale test mock
+type: module
+description: Provides mock projects with translation for locale interface translation tests.
+core: 8.x
+#hidden: true
diff --git a/core/modules/locale/tests/modules/locale_test_mock/locale_test_mock.locale_mock_projects.yml b/core/modules/locale/tests/modules/locale_test_mock/locale_test_mock.locale_mock_projects.yml
new file mode 100644
index 0000000000..22297397cf
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/locale_test_mock.locale_mock_projects.yml
@@ -0,0 +1,52 @@
+# Contains the repository of mock projects with their translation source files.
+#
+# Supported keys:
+# - 'name', 'version', 'core': As usual in .info.yml files.
+# - 'interface translation server pattern': Supports extra placeholders, see below.
+# - 'translations': Translation file data keyed by language code.
+#   - 'local': Drupal path to the mock local translation source file. See below for placeholders.
+#   - 'remote': Drupal path to the mock remote translation source file. See below for placeholders.
+# Extra placeholders:
+# - '%remote_translations_url': Domain name and base url for mock remote translations.
+# - '%module': Drupal path to the root of the module.
+# Not supported keys:
+# - 'project' and 'interface translation project': Mock projects always use the project machine name as their 'project'.
+# - 'type': Not relevant for interface translation. Always 'module'.
+# - 'datestamp': Not relevant for interface translation. Fixed date.
+
+contrib_module_one:
+  name: Contributed module one
+  version: 8.x-1.1
+  core: 8.x
+  interface translation server pattern: %remote_translations_url/%core/%project/%project-%version.%language.po
+  translations:
+    de:
+      local: %module/mock_data/local_translations/contrib_module_one-8.x-1.1.de.po
+      remote: %module/mock_data/remote_translations/contrib_module_one-8.x-1.1.de.po
+
+contrib_module_two:
+  name: Contributed module two
+  version: 8.x-2.0-beta4
+  core: 8.x
+  interface translation server pattern: %remote_translations_url/%core/%project/%project-%version.%language.po
+  translations:
+    de:
+      local: %module/mock_data/local_translations/contrib_module_two-8.x-2.0-beta4.de.po
+      remote: %module/mock_data/remote_translations/contrib_module_two-8.x-2.0-beta4.de.po
+
+contrib_module_three:
+  name: Contributed module three
+  core: 8.x
+  interface translation server pattern: %remote_translations_url/%core/%project/%project-%version.%language.po
+  translations:
+    de:
+      local: %module/mock_data/local_translations/contrib_module_three-8.x-1.0.de.po
+      remote: %module/mock_data/remote_translations/contrib_module_three-8.x-1.0.de.po
+
+custom_module_one:
+  name: Custom module one
+  core: 8.x
+  interface translation server pattern: translations://custom_module_one.%language.po
+  translations:
+    de:
+      local: %module/mock_data/local_translations/custom_module_one.de.po
diff --git a/core/modules/locale/tests/modules/locale_test_mock/locale_test_mock.module b/core/modules/locale/tests/modules/locale_test_mock/locale_test_mock.module
new file mode 100644
index 0000000000..691f82c7aa
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/locale_test_mock.module
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Hook implementations for locale_test_mock module.
+ */
+
+/**
+ * Implements hook_locale_translation_projects_alter().
+ */
+function locale_test_mock_locale_translation_projects_alter(&$projects) {
+
+  if (!\Drupal::state()->get('locale_test_mock.use_mock_projects')) {
+    return;
+  }
+
+  if (\Drupal::state()->get('locale_test_mock.hide_non_mock_projects')) {
+    $projects = array();
+  }
+
+  /** @var \Drupal\locale_test_mock\MockProjectManagerInterface $mockManager */
+  $mockManager = \Drupal::service('plugin.manager.locale_mock_project');
+  foreach ($mockManager->getEnabledProjects() as $id => $mockProject) {
+    $projects[$id] = $mockProject->getStructuredProjectInfo();
+  }
+}
diff --git a/core/modules/locale/tests/modules/locale_test_mock/locale_test_mock.routing.yml b/core/modules/locale/tests/modules/locale_test_mock/locale_test_mock.routing.yml
new file mode 100644
index 0000000000..2a831f988b
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/locale_test_mock.routing.yml
@@ -0,0 +1,6 @@
+locale_test_mock.remote_translation:
+  path: '/remote_translations'
+  defaults:
+    _controller: '\Drupal\locale_test_mock\Controller\MockTranslationController::remote'
+  requirements:
+    _permission: 'access content'
diff --git a/core/modules/locale/tests/modules/locale_test_mock/locale_test_mock.services.yml b/core/modules/locale/tests/modules/locale_test_mock/locale_test_mock.services.yml
new file mode 100644
index 0000000000..3ee0c8193a
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/locale_test_mock.services.yml
@@ -0,0 +1,8 @@
+services:
+  plugin.manager.locale_mock_project:
+    class: Drupal\locale_test_mock\MockProjectManager
+    arguments: ['@module_handler', '@cache.discovery', '@state']
+  locale_test_mock.remote_translations.path_processor:
+    class: Drupal\locale_test_mock\PathProcessor
+    tags:
+      - { name: path_processor_inbound, priority: 500 }
diff --git a/core/modules/locale/tests/modules/locale_test_mock/mock_data/local_translations/contrib_module_one-8.x-1.1.de.po b/core/modules/locale/tests/modules/locale_test_mock/mock_data/local_translations/contrib_module_one-8.x-1.1.de.po
new file mode 100644
index 0000000000..adf6f55b39
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/mock_data/local_translations/contrib_module_one-8.x-1.1.de.po
@@ -0,0 +1,16 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Drupal 8\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=UTF-8\\n"
+"Content-Transfer-Encoding: 8bit\\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
+
+msgid "January"
+msgstr "Januar_1"
+
+msgid "February"
+msgstr "Februar_1"
+
+msgid "March"
+msgstr "Marz_1"
diff --git a/core/modules/locale/tests/modules/locale_test_mock/mock_data/local_translations/contrib_module_three-8.x-1.0.de.po b/core/modules/locale/tests/modules/locale_test_mock/mock_data/local_translations/contrib_module_three-8.x-1.0.de.po
new file mode 100644
index 0000000000..e4bd97f08e
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/mock_data/local_translations/contrib_module_three-8.x-1.0.de.po
@@ -0,0 +1,16 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Drupal 8\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=UTF-8\\n"
+"Content-Transfer-Encoding: 8bit\\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
+
+msgid "April"
+msgstr "April_3"
+
+msgid "May"
+msgstr "Mai_3"
+
+msgid "June"
+msgstr "Juni_3"
diff --git a/core/modules/locale/tests/modules/locale_test_mock/mock_data/local_translations/contrib_module_two-8.x-2.0-beta4.de.po b/core/modules/locale/tests/modules/locale_test_mock/mock_data/local_translations/contrib_module_two-8.x-2.0-beta4.de.po
new file mode 100644
index 0000000000..8f97c4fdb5
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/mock_data/local_translations/contrib_module_two-8.x-2.0-beta4.de.po
@@ -0,0 +1,16 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Drupal 8\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=UTF-8\\n"
+"Content-Transfer-Encoding: 8bit\\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
+
+msgid "February"
+msgstr "Februar_2"
+
+msgid "March"
+msgstr "Marz_2"
+
+msgid "April"
+msgstr "April_2"
diff --git a/core/modules/locale/tests/modules/locale_test_mock/mock_data/local_translations/custom_module_one.de.po b/core/modules/locale/tests/modules/locale_test_mock/mock_data/local_translations/custom_module_one.de.po
new file mode 100644
index 0000000000..cb6191d532
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/mock_data/local_translations/custom_module_one.de.po
@@ -0,0 +1,7 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Drupal 8\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=UTF-8\\n"
+"Content-Transfer-Encoding: 8bit\\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
diff --git a/core/modules/locale/tests/modules/locale_test_mock/mock_data/remote_translations/contrib_module_one-8.x-1.1.de.po b/core/modules/locale/tests/modules/locale_test_mock/mock_data/remote_translations/contrib_module_one-8.x-1.1.de.po
new file mode 100644
index 0000000000..adf6f55b39
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/mock_data/remote_translations/contrib_module_one-8.x-1.1.de.po
@@ -0,0 +1,16 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Drupal 8\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=UTF-8\\n"
+"Content-Transfer-Encoding: 8bit\\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
+
+msgid "January"
+msgstr "Januar_1"
+
+msgid "February"
+msgstr "Februar_1"
+
+msgid "March"
+msgstr "Marz_1"
diff --git a/core/modules/locale/tests/modules/locale_test_mock/mock_data/remote_translations/contrib_module_three-8.x-1.0.de.po b/core/modules/locale/tests/modules/locale_test_mock/mock_data/remote_translations/contrib_module_three-8.x-1.0.de.po
new file mode 100644
index 0000000000..e4bd97f08e
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/mock_data/remote_translations/contrib_module_three-8.x-1.0.de.po
@@ -0,0 +1,16 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Drupal 8\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=UTF-8\\n"
+"Content-Transfer-Encoding: 8bit\\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
+
+msgid "April"
+msgstr "April_3"
+
+msgid "May"
+msgstr "Mai_3"
+
+msgid "June"
+msgstr "Juni_3"
diff --git a/core/modules/locale/tests/modules/locale_test_mock/mock_data/remote_translations/contrib_module_two-8.x-2.0-beta4.de.po b/core/modules/locale/tests/modules/locale_test_mock/mock_data/remote_translations/contrib_module_two-8.x-2.0-beta4.de.po
new file mode 100644
index 0000000000..8f97c4fdb5
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/mock_data/remote_translations/contrib_module_two-8.x-2.0-beta4.de.po
@@ -0,0 +1,16 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Drupal 8\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=UTF-8\\n"
+"Content-Transfer-Encoding: 8bit\\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
+
+msgid "February"
+msgstr "Februar_2"
+
+msgid "March"
+msgstr "Marz_2"
+
+msgid "April"
+msgstr "April_2"
diff --git a/core/modules/locale/tests/modules/locale_test_mock/src/Controller/MockTranslationController.php b/core/modules/locale/tests/modules/locale_test_mock/src/Controller/MockTranslationController.php
new file mode 100644
index 0000000000..56b629346f
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/src/Controller/MockTranslationController.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\locale_test_mock\Controller;
+
+use DateTime;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\locale_test_mock\MockProjectManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * Class RemoteTranslationController.
+ */
+class MockTranslationController extends ControllerBase {
+
+  /**
+   * Local mock project manager.
+   *
+   * @var \Drupal\locale_test_mock\MockProjectManagerInterface
+   */
+  protected $localMockProjectManager;
+
+  /**
+   * Constructs a new RemoteTranslationController object.
+   */
+  public function __construct(MockProjectManagerInterface $local_mock_project_manager) {
+    $this->localMockProjectManager = $local_mock_project_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.locale_mock_project')
+    );
+  }
+
+  /**
+   * @todo Documentation.
+   *
+   * @return Response|NULL
+   *   Response
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+   */
+  public function remote() {
+    $translationInfo = NULL;
+    $requestUri = \Drupal::request()->getUri();
+    $enabledTranslations = $this->localMockProjectManager->getEnabledRemoteTranslations();
+
+    foreach ($enabledTranslations as $projectId => $info) {
+      if ($requestUri == $info['uri']) {
+        if (file_exists($info['source'])) {
+          $translationInfo = $info;
+          break;
+        }
+      }
+    }
+
+    if (empty($translationInfo)) {
+      $response = new Response();
+      $response->setStatusCode(404);
+      return $response;
+    }
+
+    $date = new DateTime();
+    $date->setTimestamp($translationInfo['timestamp']);
+    $response = new Response();
+    $response
+      ->setLastModified($date)
+      ->setMaxAge(0)
+      ->setContent(file_get_contents($translationInfo['source']));
+    return $response;
+  }
+
+}
diff --git a/core/modules/locale/tests/modules/locale_test_mock/src/MockProject.php b/core/modules/locale/tests/modules/locale_test_mock/src/MockProject.php
new file mode 100644
index 0000000000..e161ecd24f
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/src/MockProject.php
@@ -0,0 +1,236 @@
+<?php
+
+namespace Drupal\locale_test_mock;
+
+use Drupal\Core\StreamWrapper\PublicStream;
+
+class MockProject implements MockProjectInterface {
+
+  /**
+   * The project's machine readable name.
+   *
+   * @var string
+   */
+  protected $projectId;
+
+  /**
+   * The project label.
+   *
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * The project version.
+   *
+   * @var string
+   */
+  protected $version;
+
+  /**
+   * The project's core compatibility.
+   *
+   * @var string
+   */
+  protected $core;
+
+  /**
+   * The project label.
+   *
+   * @var string
+   */
+  protected $interfaceTranslationServerPattern;
+
+  /**
+   * The translation source files.
+   *
+   * @var array
+   */
+  protected $translations;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createFromDefinition($definition) {
+
+    return new static(
+      $definition['id'],
+      $definition['name'],
+      $definition['version'],
+      $definition['core'],
+      $definition['interface translation server pattern'],
+      $definition['translations']
+    );
+  }
+
+  public function __construct($id, $name, $version, $core, $interface_translation_server_pattern, $translations) {
+    $this->projectId = $id;
+    $this->label = $name;
+    $this->version = $version;
+    $this->core = $core;
+    $this->interfaceTranslationServerPattern = $interface_translation_server_pattern;
+    $this->translations = $translations;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getStructuredProjectInfo() {
+
+    return [
+      'name' => $this->projectId,
+      'info' => [
+        'name' => $this->label,
+        'interface translation server pattern' => $this->interfaceTranslationServerPattern,
+        'package' => 'Other',
+        'version' => $this->version,
+        'project' => $this->projectId,
+        'datestamp' => '1514293195'
+      ],
+      'datestamp' => '1514293195',
+      'project_type' => 'module',
+      'project_status' => TRUE,
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  // @todo Does this method belong to the MockProject or the MockProjectManager?
+  public function enableLocalTranslation($langcode, $timestamp) {
+    $sourcePath = $this->getLocalTranslationSourcePath($langcode);
+
+    if (empty($sourcePath)) {
+      return '';
+    }
+
+    if (!file_exists($sourcePath)) {
+      throw new \RuntimeException(sprintf('Local source file not found at %s.', $sourcePath));
+    }
+
+    // @todo Only copy when the file is placed in translations:// or public://
+    $path = file_unmanaged_copy($sourcePath, $this->getLocalTranslationPath($langcode), FILE_EXISTS_REPLACE);
+    if (!$path) {
+      throw new \RuntimeException(sprintf('Unable to copy %s to directory %s.', basename($sourcePath), $this->getLocalTranslationPath()));
+    }
+    touch($path, (int) $timestamp);
+
+    return hash_file('md5', $path);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  // @todo Does this method belong to the MockProject or the MockProjectManager?
+  public function enableRemoteTranslation($langcode, $timestamp) {
+    $sourcePath = $this->getRemoteTranslationSourcePath($langcode);
+
+    if (empty($sourcePath)) {
+      return NULL;
+    }
+
+    $data = [
+      'uri' => $this->getRemoteTranslationUri($langcode),
+      'source' => $sourcePath,
+      'timestamp' => $timestamp,
+    ];
+    // @todo DI.
+    \Drupal::state()
+      ->set("locale_test_mock.remote_translation.{$this->projectId}.{$langcode}", $data);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLocalTranslationPath($langcode) {
+    if ($this->isRemote($this->interfaceTranslationServerPattern)) {
+      $path = 'translations://';
+    }
+    else {
+      $path = $this->interfaceTranslationServerPattern;
+    }
+
+    return $this->replacePlaceholders($langcode, $path);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRemoteTranslationUri($langcode) {
+    $uri = $this->interfaceTranslationServerPattern;
+
+    if (!$this->isRemote($uri)) {
+      return '';
+    }
+
+    return $this->replacePlaceholders($langcode, $uri);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLocalTranslationSourcePath($langcode) {
+    if (!isset($this->translations[$langcode])) {
+      throw new \RuntimeException(sprintf('Unknown translation language code "%s" for plugin "%s"', $langcode, $this->projectId));
+    }
+    return $this->translations[$langcode]['local'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRemoteTranslationSourcePath($langcode) {
+    if (!isset($this->translations[$langcode])) {
+      throw new \RuntimeException(sprintf('Unknown translation language code "%s" for plugin "%s"', $langcode, $this->projectId));
+    }
+    return $this->translations[$langcode]['remote'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTranslationLangcodes() {
+    return array_keys($this->translations);
+  }
+
+  /**
+   * Determine if a file uri is remote.
+   *
+   * @param string $uri
+   *   The URI or URI pattern of the file.
+   *
+   * @return bool
+   *   TRUE if the $uri is a remote file.
+   */
+  private function isRemote($uri) {
+    return (bool) preg_match('/^http(s?):/', $uri);
+  }
+
+  /**
+   * Build path to translation file using a path with placeholders.
+   *
+   * @param string $langcode
+   *   The language code.
+   * @param string $uri
+   *   URI string containing placeholders. Available placeholders:
+   *   - "%project": Project name.
+   *   - "%version": Project version.
+   *   - "%core": Project core version.
+   *   - "%language": Language code.
+   *
+   * @return string
+   *   String with replaced placeholders.
+   */
+  protected function replacePlaceholders($langcode, $uri) {
+
+    $variables = array(
+      '%project' => $this->projectId,
+      '%version' => $this->version,
+      '%core' => $this->core,
+      '%language' => $langcode,
+    );
+    return strtr($uri, $variables);
+  }
+
+
+}
diff --git a/core/modules/locale/tests/modules/locale_test_mock/src/MockProjectInterface.php b/core/modules/locale/tests/modules/locale_test_mock/src/MockProjectInterface.php
new file mode 100644
index 0000000000..e5b90e8f01
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/src/MockProjectInterface.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace Drupal\locale_test_mock;
+
+interface MockProjectInterface {
+
+  /**
+   * Creates a MockProject from locale_mock_project data.
+   *
+   * @param $definition
+   *   A plugin definition.
+   *
+   * @return \Drupal\locale_test_mock\MockProjectInterface
+   */
+  public static function createFromDefinition($definition);
+
+  /**
+   * The project info as required by hook_locale_translation_projects_alter().
+   *
+   * @return array
+   */
+  public function getStructuredProjectInfo();
+
+  /**
+   * Enables mocking of the local translation file.
+   *
+   * @param string $langcode
+   *   The language code of the translation file.
+   * @param int $timestamp
+   *   The timestamp to set.
+   *
+   * @return string
+   *   The MD5 hash of the copied file.
+   *
+   * @throws \RuntimeException
+   *   When the requested file is not found or can not be written.
+   */
+  public function enableLocalTranslation($langcode, $timestamp);
+
+  /**
+   * Enables mocking of the remote translation file.
+   *
+   * @param string $langcode
+   *   The language code.
+   * @param int $timestamp
+   *   The timestamp to set.
+   */
+  public function enableRemoteTranslation($langcode, $timestamp);
+
+  /**
+   * Returns the relative path of the mock local translations directory.
+   *
+   * @param string $langcode
+   *   The language code.
+   *
+   * @return string
+   *   The path of the directory.
+   */
+  public function getLocalTranslationPath($langcode);
+
+  /**
+   * Returns the URI of the remote translation.
+   *
+   * @param string $langcode
+   *   The language code.
+   *
+   * @return string
+   */
+  public function getRemoteTranslationUri($langcode);
+
+  /**
+   * Returns the path of the local translation source file.
+   *
+   * @param string $langcode
+   *   The language code.
+   *
+   * @return string
+   *   The drupal path of the file.
+   */
+  public function getLocalTranslationSourcePath($langcode);
+
+  /**
+   * Returns the path of the remote translation source file.
+   *
+   * @param string $langcode
+   *   The language code.
+   *
+   * @return string
+   *   The drupal path of the file.
+   */
+  public function getRemoteTranslationSourcePath($langcode);
+
+  /**
+   * Returns the language codes of the available translations.
+   *
+   * @return array
+   */
+  public function getTranslationLangcodes();
+
+}
diff --git a/core/modules/locale/tests/modules/locale_test_mock/src/MockProjectManager.php b/core/modules/locale/tests/modules/locale_test_mock/src/MockProjectManager.php
new file mode 100644
index 0000000000..757ced61db
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/src/MockProjectManager.php
@@ -0,0 +1,182 @@
+<?php
+
+namespace Drupal\locale_test_mock;
+
+use Drupal\Component\Plugin\Exception\PluginException;
+use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
+use Drupal\Core\Plugin\Discovery\YamlDiscovery;
+use Drupal\Core\State\StateInterface;
+use Drupal\Core\Url;
+
+/**
+ * Provides the default locale_mock_project manager.
+ */
+class MockProjectManager extends DefaultPluginManager implements MockProjectManagerInterface {
+
+  /**
+   * The state storage service.
+   *
+   * @var \Drupal\Core\State\StateInterface
+   */
+  protected $state;
+
+  /**
+   * Provides default values for all locale_mock_project plugins.
+   *
+   * @var array
+   */
+  protected $defaults = [
+    'name' => '',
+    'version' => '',
+    'core' => '',
+    'interface translation server pattern' => '%remote_translations_url/%core/%project/%project-%version.%language.po',
+    'translations' => [],
+  ];
+
+  /**
+   * Constructs a new MockProjectManager object.
+   *
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   */
+  public function __construct(ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, StateInterface $state) {
+    $this->moduleHandler = $module_handler;
+    $this->setCacheBackend($cache_backend, 'locale_mock_project', ['locale_mock_project']);
+    $this->state = $state;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDiscovery() {
+    if (!isset($this->discovery)) {
+      $yaml_discovery = new YamlDiscovery('locale_mock_projects', $this->moduleHandler->getModuleDirectories());
+      $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml_discovery);
+    }
+    return $this->discovery;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processDefinition(&$definition, $plugin_id) {
+    parent::processDefinition($definition, $plugin_id);
+
+    $translations = [];
+    foreach ($definition['translations'] as $langcode => $translation) {
+      $translation += [
+        'local' => '',
+        'remote' => '',
+      ];
+      $translations[$translations[$langcode]] = $translation;
+    }
+
+    $replacements = [
+      '%module' => \Drupal::moduleHandler()->getModule('locale_test_mock')->getPath(),
+      '%remote_translations_url' => Url::fromRoute('locale_test_mock.remote_translation')->setAbsolute()->toString(),
+    ];
+    $definition['interface translation server pattern'] = strtr($definition['interface translation server pattern'], $replacements);
+    foreach ($definition['translations'] as $langode => $translation) {
+      foreach (['local', 'remote'] as $key) {
+        $definition['translations'][$langode][$key] = strtr($definition['translations'][$langode][$key], $replacements);
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProjectNames() {
+    $projects = [];
+
+    foreach ($this->getDefinitions() as $definition) {
+      $projects[$definition['id']] = $definition['name'];
+    }
+
+    return $projects;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMockProject($pluginId) {
+    // @todo Cache if need to.
+    if ($definition = $this->getDefinition($pluginId)) {
+      return MockProject::createFromDefinition($definition);
+    }
+    throw new PluginNotFoundException(sprintf('Unknown plugin %s', $pluginId));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEnabledProjects() {
+    $enabled = [];
+
+    foreach (array_keys($this->getProjectNames()) as $projectId) {
+      if ($this->state->get("locale_test_mock.project.{$projectId}", FALSE)) {
+        $enabled[$projectId] = $this->getMockProject($projectId);
+      }
+    }
+
+    return $enabled;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEnabledRemoteTranslations() {
+    $enabled = [];
+
+    foreach (array_keys($this->getProjectNames()) as $projectId) {
+      $project = $this->getMockProject($projectId);
+      foreach ($project->getTranslationLangcodes() as $langcode) {
+        if ($data = $this->state->get("locale_test_mock.remote_translation.{$projectId}.{$langcode}", FALSE)) {
+          $enabled[] = $data;
+        }
+      }
+    }
+
+    return $enabled;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function enableMockProject($projectId) {
+    $projects = $this->getProjectNames();
+
+    if (!isset($projects[$projectId])) {
+      throw new \RuntimeException(sprintf('Unknown mock project %s', $projectId));
+    }
+
+    $this->state->set("locale_test_mock.project.{$projectId}", TRUE);
+  }
+
+  // @todo BELOW
+  /**
+   * Creates the directory for local translation files.
+   *
+   * @throws \RuntimeException
+   *   When the directory can not be created.
+   */
+  protected function prepareLocalTranslationDirectory() {
+
+    if (!$this->preparedLocalTranslationsDirectory) {
+      $directory = $this->getLocalTranslationPath();
+      $result = file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+      if (!$result) {
+        throw new RuntimeException(sprintf('Unable to create directory %s.', $directory));
+      }
+    }
+
+    $this->preparedLocalTranslationsDirectory = TRUE;
+  }
+
+}
diff --git a/core/modules/locale/tests/modules/locale_test_mock/src/MockProjectManagerInterface.php b/core/modules/locale/tests/modules/locale_test_mock/src/MockProjectManagerInterface.php
new file mode 100644
index 0000000000..a09490f8fd
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/src/MockProjectManagerInterface.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\locale_test_mock;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+
+/**
+ * Defines an interface for locale_mock_project managers.
+ */
+interface MockProjectManagerInterface extends PluginManagerInterface {
+
+  /**
+   * The names of the available projects.
+   *
+   * @return array
+   *   Structured array of project names, keyed by project ID.
+   */
+  public function getProjectNames();
+
+  /**
+   * Returns a mock project object with data from a locale_mock_project plugin.
+   *
+   * @var string $pluginId
+   *
+   * @return \Drupal\locale_test_mock\MockProjectInterface
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+   *   Thrown if $pluginId is invalid.
+   */
+  public function getMockProject($pluginId);
+
+  /**
+   * Returns the project IDs of mock enabled projects.
+   *
+   * @return \Drupal\locale_test_mock\MockProjectInterface[]
+   *   Structured array of enabled Mock projects, keyed by project ID.
+   */
+  public function getEnabledProjects();
+
+  /**
+   * Returns the data of mock enabled remote translations.
+   *
+   * @return array
+   *   Structured array of remote translation data keyed by its project ID.
+   *
+   * @see self::enableRemoteTranslation.
+   */
+  public function getEnabledRemoteTranslations();
+
+  /**
+   * Enable a project for mocking.
+   *
+   * @param string $projectId
+   *   Project ID.
+   */
+  public function enableMockProject($projectId);
+
+}
diff --git a/core/modules/locale/tests/modules/locale_test_mock/src/PathProcessor.php b/core/modules/locale/tests/modules/locale_test_mock/src/PathProcessor.php
new file mode 100644
index 0000000000..4226db0651
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_mock/src/PathProcessor.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\locale_test_mock;
+
+use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Path processor for mock remote translations.
+ */
+class PathProcessor implements InboundPathProcessorInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processInbound($path, Request $request) {
+    if (strpos($path, '/remote_translations/') === 0) {
+      $path = '/remote_translations';
+    }
+
+    return $path;
+  }
+
+}
