diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index 43d15a6..17ac5a9 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -651,9 +651,14 @@ public function processForm($form_id, &$form, &$form_state) {
         $form_state['executed'] = TRUE;
 
         // Redirect the form based on values in $form_state.
-        $redirect = $this->redirectForm($form_state);
-        if (is_object($redirect)) {
-          return $redirect;
+        if ($redirect = $this->redirectForm($form_state)) {
+          $form_state['response'] = $redirect;
+        }
+
+        // If there is a response in form_state, respect that instead of doing
+        // a redirect.
+        if (isset($form_state['response']) && $form_state['response'] instanceof Response) {
+          return $form_state['response'];
         }
       }
 
diff --git a/core/lib/Drupal/Core/Form/FormBuilderInterface.php b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
index ad6febf..8a102f9 100644
--- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php
+++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
@@ -114,6 +114,12 @@ public function getForm($form_arg);
    *     already set $form_state['rebuild'] to cause the form processing to
    *     bypass submit handlers and rebuild the form instead, even if there are
    *     no validation errors.
+   *   - response: Used when a form needs to return some kind of a
+   *     \Symfony\Component\HttpFoundation\Response object, e.g., a
+   *     \Symfony\Component\HttpFoundation\BinaryFileResponse when triggering a
+   *     file download. If you use the $form_state['redirect'] key, it will be
+   *     used to build a \Symfony\Component\HttpFoundation\RedirectResponse and
+   *     will populate this key.
    *   - redirect: Used to redirect the form on submission. It may either be a
    *     string containing the destination URL, or an array of arguments
    *     compatible with url(). See url() for complete information.
diff --git a/core/modules/locale/lib/Drupal/locale/Form/ExportForm.php b/core/modules/locale/lib/Drupal/locale/Form/ExportForm.php
new file mode 100644
index 0000000..433c8c9
--- /dev/null
+++ b/core/modules/locale/lib/Drupal/locale/Form/ExportForm.php
@@ -0,0 +1,178 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\locale\Form\ExportForm.
+ */
+
+namespace Drupal\locale\Form;
+
+use Drupal\Component\Gettext\PoStreamWriter;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Language\Language;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\locale\PoDatabaseReader;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\BinaryFileResponse;
+
+/**
+ * Form for the Gettext translation files export form.
+ */
+class ExportForm extends FormBase {
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * Constructs a new ExportForm.
+   *
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   */
+  public function __construct(LanguageManagerInterface $language_manager) {
+    $this->languageManager = $language_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('language_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'locale_translate_export_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state) {
+    $languages = $this->languageManager->getLanguages();
+    $language_options = array();
+    foreach ($languages as $langcode => $language) {
+      if ($langcode != 'en' || locale_translate_english()) {
+        $language_options[$langcode] = $language->name;
+      }
+    }
+    $language_default = $this->languageManager->getDefaultLanguage();
+
+    if (empty($language_options)) {
+      $form['langcode'] = array(
+        '#type' => 'value',
+        '#value' => Language::LANGCODE_SYSTEM,
+      );
+      $form['langcode_text'] = array(
+        '#type' => 'item',
+        '#title' => $this->t('Language'),
+        '#markup' => $this->t('No language available. The export will only contain source strings.'),
+      );
+    }
+    else {
+      $form['langcode'] = array(
+        '#type' => 'select',
+        '#title' => $this->t('Language'),
+        '#options' => $language_options,
+        '#default_value' => $language_default->id,
+        '#empty_option' => $this->t('Source text only, no translations'),
+        '#empty_value' => Language::LANGCODE_SYSTEM,
+      );
+      $form['content_options'] = array(
+        '#type' => 'details',
+        '#title' => $this->t('Export options'),
+        '#collapsed' => TRUE,
+        '#tree' => TRUE,
+        '#states' => array(
+          'invisible' => array(
+            ':input[name="langcode"]' => array('value' => Language::LANGCODE_SYSTEM),
+          ),
+        ),
+      );
+      $form['content_options']['not_customized'] = array(
+        '#type' => 'checkbox',
+        '#title' => $this->t('Include non-customized translations'),
+        '#default_value' => TRUE,
+      );
+      $form['content_options']['customized'] = array(
+        '#type' => 'checkbox',
+        '#title' => $this->t('Include customized translations'),
+        '#default_value' => TRUE,
+      );
+      $form['content_options']['not_translated'] = array(
+        '#type' => 'checkbox',
+        '#title' => $this->t('Include untranslated text'),
+        '#default_value' => TRUE,
+      );
+    }
+
+    $form['actions'] = array(
+      '#type' => 'actions'
+    );
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Export')
+    );
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    // If template is required, language code is not given.
+    if ($form_state['values']['langcode'] != Language::LANGCODE_SYSTEM) {
+      $language = $this->languageManager->getLanguage($form_state['values']['langcode']);
+    }
+    else {
+      $language = NULL;
+    }
+    $content_options = isset($form_state['values']['content_options']) ? $form_state['values']['content_options'] : array();
+    $reader = new PoDatabaseReader();
+    $languageName = '';
+    if ($language != NULL) {
+      $reader->setLangcode($language->id);
+      $reader->setOptions($content_options);
+      $languages = $this->languageManager->getLanguages();
+      $languageName = isset($languages[$language->id]) ? $languages[$language->id]->name : '';
+      $filename = $language->id .'.po';
+    }
+    else {
+      // Template required.
+      $filename = 'drupal.pot';
+    }
+
+    $item = $reader->readItem();
+    if (!empty($item)) {
+      $uri = tempnam('temporary://', 'po_');
+      $header = $reader->getHeader();
+      $header->setProjectName($this->config('system.site')->get('name'));
+      $header->setLanguageName($languageName);
+
+      $writer = new PoStreamWriter;
+      $writer->setUri($uri);
+      $writer->setHeader($header);
+
+      $writer->open();
+      $writer->writeItem($item);
+      $writer->writeItems($reader);
+      $writer->close();
+
+      $response = new BinaryFileResponse($uri);
+      $response->setContentDisposition('attachment', $filename);
+      $form_state['response'] = $response;
+    }
+    else {
+      drupal_set_message($this->t('Nothing to export.'));
+    }
+  }
+
+}
diff --git a/core/modules/locale/lib/Drupal/locale/Form/LocaleForm.php b/core/modules/locale/lib/Drupal/locale/Form/LocaleForm.php
index c06a4aa..aaa0bfa 100644
--- a/core/modules/locale/lib/Drupal/locale/Form/LocaleForm.php
+++ b/core/modules/locale/lib/Drupal/locale/Form/LocaleForm.php
@@ -22,16 +22,6 @@ public function import() {
   }
 
   /**
-   * Wraps locale_translate_export_form().
-   *
-   * @todo Remove locale_translate_export_form().
-   */
-  public function export() {
-    module_load_include('bulk.inc', 'locale');
-    return drupal_get_form('locale_translate_export_form');
-  }
-
-  /**
    * Wraps locale_translation_status_form().
    *
    * @todo Remove locale_translation_status_form().
diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc
index c0e7bbe..01d4960 100644
--- a/core/modules/locale/locale.bulk.inc
+++ b/core/modules/locale/locale.bulk.inc
@@ -5,9 +5,7 @@
  * Mass import-export and batch import functionality for Gettext .po files.
  */
 
-use Drupal\Component\Gettext\PoStreamWriter;
 use Drupal\locale\Gettext;
-use Drupal\locale\PoDatabaseReader;
 use Drupal\Core\Language\Language;
 use Symfony\Component\HttpFoundation\BinaryFileResponse;
 use Drupal\file\FileInterface;
@@ -148,135 +146,6 @@ function locale_translate_import_form_submit($form, &$form_state) {
 }
 
 /**
- * Form constructor for the Gettext translation files export form.
- *
- * @see locale_translate_export_form_submit()
- * @ingroup forms
- *
- * @deprecated Use \Drupal\locale\Form\LocaleForm::export()
- */
-function locale_translate_export_form($form, &$form_state) {
-  $languages = language_list();
-  $language_options = array();
-  foreach ($languages as $langcode => $language) {
-    if ($langcode != 'en' || locale_translate_english()) {
-      $language_options[$langcode] = $language->name;
-    }
-  }
-  $language_default = language_default();
-
-  if (empty($language_options)) {
-    $form['langcode'] = array(
-      '#type' => 'value',
-      '#value' => Language::LANGCODE_SYSTEM,
-    );
-    $form['langcode_text'] = array(
-      '#type' => 'item',
-      '#title' => t('Language'),
-      '#markup' => t('No language available. The export will only contain source strings.'),
-    );
-  }
-  else {
-    $form['langcode'] = array(
-      '#type' => 'select',
-      '#title' => t('Language'),
-      '#options' => $language_options,
-      '#default_value' => $language_default->id,
-      '#empty_option' => t('Source text only, no translations'),
-      '#empty_value' => Language::LANGCODE_SYSTEM,
-    );
-    $form['content_options'] = array(
-      '#type' => 'details',
-      '#title' => t('Export options'),
-      '#collapsed' => TRUE,
-      '#tree' => TRUE,
-      '#states' => array(
-        'invisible' => array(
-          ':input[name="langcode"]' => array('value' => Language::LANGCODE_SYSTEM),
-        ),
-      ),
-    );
-    $form['content_options']['not_customized'] = array(
-      '#type' => 'checkbox',
-      '#title' => t('Include non-customized translations'),
-      '#default_value' => TRUE,
-    );
-    $form['content_options']['customized'] = array(
-      '#type' => 'checkbox',
-      '#title' => t('Include customized translations'),
-      '#default_value' => TRUE,
-    );
-    $form['content_options']['not_translated'] = array(
-      '#type' => 'checkbox',
-      '#title' => t('Include untranslated text'),
-      '#default_value' => TRUE,
-    );
-  }
-
-  $form['actions'] = array(
-    '#type' => 'actions'
-  );
-  $form['actions']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Export')
-  );
-  return $form;
-}
-
-/**
- * Form submission handler for locale_translate_export_form().
- */
-function locale_translate_export_form_submit($form, &$form_state) {
-  // If template is required, language code is not given.
-  if ($form_state['values']['langcode'] != Language::LANGCODE_SYSTEM) {
-    $language = language_load($form_state['values']['langcode']);
-  }
-  else {
-    $language = NULL;
-  }
-  $content_options = isset($form_state['values']['content_options']) ? $form_state['values']['content_options'] : array();
-  $reader = new PoDatabaseReader();
-  $languageName = '';
-  if ($language != NULL) {
-    $reader->setLangcode($language->id);
-    $reader->setOptions($content_options);
-    $languages = language_list();
-    $languageName = isset($languages[$language->id]) ? $languages[$language->id]->name : '';
-    $filename = $language->id .'.po';
-  }
-  else {
-    // Template required.
-    $filename = 'drupal.pot';
-  }
-
-  $item = $reader->readItem();
-  if (!empty($item)) {
-    $uri = tempnam('temporary://', 'po_');
-    $header = $reader->getHeader();
-    $header->setProjectName(\Drupal::config('system.site')->get('name'));
-    $header->setLanguageName($languageName);
-
-    $writer = new PoStreamWriter;
-    $writer->setUri($uri);
-    $writer->setHeader($header);
-
-    $writer->open();
-    $writer->writeItem($item);
-    $writer->writeItems($reader);
-    $writer->close();
-
-    $response = new BinaryFileResponse($uri);
-    $response->setContentDisposition('attachment', $filename);
-    // @todo remove lines below once converted to new routing system.
-    $response->prepare(\Drupal::request())
-      ->send();
-  }
-  else {
-    drupal_set_message('Nothing to export.');
-  }
-}
-
-/**
  * Prepare a batch to import all translations.
  *
  * @param array $options
diff --git a/core/modules/locale/locale.routing.yml b/core/modules/locale/locale.routing.yml
index 190ddf7..6658dad 100644
--- a/core/modules/locale/locale.routing.yml
+++ b/core/modules/locale/locale.routing.yml
@@ -32,7 +32,7 @@ locale.translate_import:
 locale.translate_export:
   path: '/admin/config/regional/translate/export'
   defaults:
-    _content: '\Drupal\locale\Form\LocaleForm::export'
+    _form: '\Drupal\locale\Form\ExportForm'
     _title: 'Export'
   requirements:
     _permission: 'translate interface'
diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
index 0b52a75..bcfdf52 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
@@ -112,6 +112,55 @@ public function testGetFormIdWithBaseForm() {
   }
 
   /**
+   * Tests the handling of $form_state['response'].
+   *
+   * @dataProvider formStateResponseProvider
+   */
+  public function testHandleFormStateResponse($class, $form_state_key) {
+    $form_id = 'test_form_id';
+    $expected_form = $form_id();
+
+    $response = $this->getMockBuilder($class)
+      ->disableOriginalConstructor()
+      ->getMock();
+    $response->expects($this->any())
+      ->method('prepare')
+      ->will($this->returnValue($response));
+
+    $form_arg = $this->getMockForm($form_id, $expected_form);
+    $form_arg->expects($this->any())
+      ->method('submitForm')
+      ->will($this->returnCallback(function ($form, &$form_state) use ($response, $form_state_key) {
+        $form_state[$form_state_key] = $response;
+      }));
+
+    $form_state = array();
+    $this->formBuilder->getFormId($form_arg, $form_state);
+
+    try {
+      $form_state['values'] = array();
+      $form_state['redirect'] = FALSE;
+      $form_state['input']['form_id'] = $form_id;
+      $this->simulateFormSubmission($form_id, $form_arg, $form_state, FALSE);
+      $this->fail('TestFormBuilder::sendResponse() was not triggered.');
+    }
+    catch (\Exception $e) {
+      $this->assertSame('exit', $e->getMessage());
+    }
+    $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $form_state['response']);
+  }
+
+  /**
+   * Provides test data for testHandleFormStateResponse().
+   */
+  public function formStateResponseProvider() {
+    return array(
+      array('Symfony\Component\HttpFoundation\Response', 'response'),
+      array('Symfony\Component\HttpFoundation\RedirectResponse', 'redirect'),
+    );
+  }
+
+  /**
    * Tests the redirectForm() method when a redirect is expected.
    *
    * @param array $form_state
diff --git a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
index 3b7a97f..44059c4 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
@@ -189,15 +189,19 @@ protected function getMockForm($form_id, $expected_form = NULL, $count = 1) {
    *   The form object.
    * @param array $form_state
    *   An associative array containing the current state of the form.
+   * @param bool $programmed
+   *   Whether $form_state['programmed'] should be set to TRUE or not. If it is
+   *   not set to TRUE, you must provide additional data in $form_state for the
+   *   submission to take place.
    *
    * @return array
    *   The built form.
    */
-  protected function simulateFormSubmission($form_id, FormInterface $form_arg, array &$form_state) {
+  protected function simulateFormSubmission($form_id, FormInterface $form_arg, array &$form_state, $programmed = TRUE) {
     $form_state['build_info']['callback_object'] = $form_arg;
     $form_state['build_info']['args'] = array();
     $form_state['input']['op'] = 'Submit';
-    $form_state['programmed'] = TRUE;
+    $form_state['programmed'] = $programmed;
     $form_state['submitted'] = TRUE;
     return $this->formBuilder->buildForm($form_id, $form_state);
   }
