diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index c0003b350a..3831dd151f 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -959,7 +959,8 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL
     // rename filename.php.foo and filename.php to filename.php.foo.txt and
     // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
     // evaluates to TRUE.
-    if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
+    $system_file_config = \Drupal::config('system.file');
+    if (!$system_file_config->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
       $file->setMimeType('text/plain');
       // The destination filename will also later be used to create the URI.
       $file->setFilename($file->getFilename() . '.txt');
@@ -989,7 +990,24 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL
     if (substr($destination, -1) != '/') {
       $destination .= '/';
     }
-    $file->destination = file_destination($destination . $file->getFilename(), $replace);
+
+    $filename = $file->getFilename();
+    // If the transliteration option is enabled, transliterate the filename.
+    if ($system_file_config->get('filename_transliteration')) {
+      // Transliterate and sanitize the destination filename.
+      $filename = \Drupal::transliteration()
+        ->transliterate($filename, $file->language()->getId(), '');
+      // Replace whitespace.
+      $filename = str_replace(' ', '-', $filename);
+      // Remove remaining unsafe characters.
+      $filename = preg_replace('![^0-9A-Za-z_.-]!', '', $filename);
+      // Remove multiple consecutive non-alphabetical characters.
+      $filename = preg_replace('/(_)_+|(\.)\.+|(-)-+/', '\\1\\2\\3', $filename);
+      // Force lowercase to prevent issues on case-insensitive file systems.
+      $filename = mb_strtolower($filename);
+    }
+    $file->destination = file_destination($destination . $filename, $replace);
+
     // If file_destination() returns FALSE then $replace === FILE_EXISTS_ERROR and
     // there's an existing file so we need to bail.
     if ($file->destination === FALSE) {
@@ -1031,6 +1049,10 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL
       continue;
     }

+    // Update the filename with any changes as a result of transliteration or
+    // renaming due to an existing file.
+    $file->setFilename(\Drupal::service('file_system')->basename($file->destination));
+
     // Set the permissions on the new file.
     drupal_chmod($file->getFileUri());

diff --git a/core/modules/file/src/Tests/FileFieldWidgetTest.php b/core/modules/file/src/Tests/FileFieldWidgetTest.php
index 3102bd744d..e1fa4e4b2c 100644
--- a/core/modules/file/src/Tests/FileFieldWidgetTest.php
+++ b/core/modules/file/src/Tests/FileFieldWidgetTest.php
@@ -259,7 +259,7 @@ public function testMultiValuedWidget() {
       '%field' => $field_name,
       '@max' => $cardinality,
       '@count' => count($upload_files_node_creation) + count($upload_files_node_revision),
-      '%list' => implode(', ', array_fill(0, 3, $test_file->getFilename())),
+      '%list' => implode(', ', ['text-0_2.txt', 'text-0_3.txt', 'text-0_4.txt']),
     ];
     $this->assertRaw(t('Field %field can only hold @max values but there were @count uploaded. The following files have been omitted as a result: %list.', $args));
     $node_storage->resetCache([$nid]);
@@ -291,7 +291,7 @@ public function testMultiValuedWidget() {
       '%field' => $field_name,
       '@max' => $cardinality,
       '@count' => count($upload_files),
-      '%list' => $test_file->getFileName(),
+      '%list' => 'text-0_12.txt',
     ];
     $this->assertRaw(t('Field %field can only hold @max values but there were @count uploaded. The following files have been omitted as a result: %list.', $args));
   }
diff --git a/core/modules/file/tests/src/Functional/SaveUploadTest.php b/core/modules/file/tests/src/Functional/SaveUploadTest.php
index bebf47e076..90b4b4578e 100644
--- a/core/modules/file/tests/src/Functional/SaveUploadTest.php
+++ b/core/modules/file/tests/src/Functional/SaveUploadTest.php
@@ -47,10 +47,17 @@ class SaveUploadTest extends FileManagedTestBase {
    */
   protected $imageExtension;

+  /**
+   * The user used by the test.
+   *
+   * @var \Drupal\user\Entity\User
+   */
+  protected $account;
+
   protected function setUp() {
     parent::setUp();
-    $account = $this->drupalCreateUser(['access site reports']);
-    $this->drupalLogin($account);
+    $this->account = $this->drupalCreateUser(['access site reports']);
+    $this->drupalLogin($this->account);

     $image_files = $this->drupalGetTestFiles('image');
     $this->image = File::create((array) current($image_files));
@@ -127,6 +134,48 @@ public function testNormal() {
     $this->assertTrue(is_file('temporary://' . $dir . '/' . trim(drupal_basename($image3_realpath))));
   }

+  /**
+   * Tests filename transliteration.
+   */
+  public function testTransliteration() {
+    $file = $this->generateFile('TEXT-Å“', 64, 5, 'text');
+
+    // Upload a file with a name with uppercase and unicode characters.
+    $edit = [
+      'files[file_test_upload]' => \Drupal::service('file_system')->realpath($file),
+      'extensions' => 'txt',
+      'is_image_file' => FALSE,
+    ];
+    $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
+    $this->assertSession()->statusCodeEquals(200);
+    // Test that the file name has not been transliterated.
+    $this->assertSession()->responseContains('File name is TEXT-Å“.txt.');
+
+    // Enable transliteration via the UI.
+    $this->drupalLogin($this->rootUser);
+    $this->drupalPostForm('admin/config/media/file-system', ['filename_transliteration' => TRUE], 'Save configuration');
+    $this->drupalLogin($this->account);
+
+    // Upload a file with a name with uppercase and unicode characters.
+    $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
+    $this->assertSession()->statusCodeEquals(200);
+    // Test that the file name has been transliterated.
+    $this->assertSession()->responseContains('File name is text-oe.txt.');
+
+    // Generate another file with a name that will be changed when
+    // transliteration is on.
+    $file = $this->generateFile('S  Pace--ðŸ™ˆ', 64, 5, 'text');
+    $edit = [
+      'files[file_test_upload]' => \Drupal::service('file_system')->realpath($file),
+      'extensions' => 'txt',
+      'is_image_file' => FALSE,
+    ];
+    $this->drupalPostForm('file-test/upload', $edit, t('Submit'));
+    $this->assertSession()->statusCodeEquals(200);
+    // Test that the file name has been transliterated.
+    $this->assertSession()->responseContains('File name is s-pace-.txt.');
+  }
+
   /**
    * Test extension handling.
    */
diff --git a/core/modules/system/config/install/system.file.yml b/core/modules/system/config/install/system.file.yml
index ec8c0533f6..73b6fd7d5c 100644
--- a/core/modules/system/config/install/system.file.yml
+++ b/core/modules/system/config/install/system.file.yml
@@ -3,3 +3,4 @@ default_scheme: 'public'
 path:
   temporary: ''
 temporary_maximum_age: 21600
+filename_transliteration: false
diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml
index a6f61b68ed..2ac9016656 100644
--- a/core/modules/system/config/schema/system.schema.yml
+++ b/core/modules/system/config/schema/system.schema.yml
@@ -281,6 +281,9 @@ system.file:
     temporary_maximum_age:
       type: integer
       label: 'Maximum age for temporary files'
+    filename_transliteration:
+      type: boolean
+      label: 'Transliterate names of uploaded files'

 system.image:
   type: config_object
diff --git a/core/modules/system/src/Form/FileSystemForm.php b/core/modules/system/src/Form/FileSystemForm.php
index b0ca6d129e..84927c6fd4 100644
--- a/core/modules/system/src/Form/FileSystemForm.php
+++ b/core/modules/system/src/Form/FileSystemForm.php
@@ -133,6 +133,13 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#description' => t('Temporary files are not referenced, but are in the file system and therefore may show up in administrative lists. <strong>Warning:</strong> If enabled, temporary files will be permanently deleted and may not be recoverable.'),
     ];

+    $form['filename_transliteration'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Enable filename transliteration'),
+      '#default_value' => $config->get('filename_transliteration'),
+      '#description' => t('Transliteration ensures that filenames do not contain unicode characters.'),
+    ];
+
     return parent::buildForm($form, $form_state);
   }

@@ -142,7 +149,8 @@ public function buildForm(array $form, FormStateInterface $form_state) {
   public function submitForm(array &$form, FormStateInterface $form_state) {
     $config = $this->config('system.file')
       ->set('path.temporary', $form_state->getValue('file_temporary_path'))
-      ->set('temporary_maximum_age', $form_state->getValue('temporary_maximum_age'));
+      ->set('temporary_maximum_age', $form_state->getValue('temporary_maximum_age'))
+      ->set('filename_transliteration', (bool) $form_state->getValue('filename_transliteration'));

     if ($form_state->hasValue('file_default_scheme')) {
       $config->set('default_scheme', $form_state->getValue('file_default_scheme'));
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 6eb8c12005..e2687cd6af 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -2173,3 +2173,12 @@ function system_update_8501() {
     }
   }
 }
+
+/**
+ * Set filename_transliteration config to the default value.
+ */
+function system_update_8701() {
+  \Drupal::configFactory()->getEditable('system.file')
+    ->set('filename_transliteration', FALSE)
+    ->save();
+}
diff --git a/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php b/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php
index 33c963de3c..dcf11f26ee 100644
--- a/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php
+++ b/core/modules/system/tests/src/Kernel/Migrate/d6/MigrateSystemConfigurationTest.php
@@ -52,6 +52,8 @@ class MigrateSystemConfigurationTest extends MigrateDrupal6TestBase {
       ],
       // temporary_maximum_age is not handled by the migration.
       'temporary_maximum_age' => 21600,
+      // filename_transliteration is not handled by migration.
+      'filename_transliteration' => FALSE,
     ],
     'system.image.gd' => [
       'jpeg_quality' => 75,
diff --git a/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php b/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php
index 0216e2dd31..c7968bf844 100644
--- a/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php
+++ b/core/modules/system/tests/src/Kernel/Migrate/d7/MigrateSystemConfigurationTest.php
@@ -50,6 +50,8 @@ class MigrateSystemConfigurationTest extends MigrateDrupal7TestBase {
       ],
       // temporary_maximum_age is not handled by the migration.
       'temporary_maximum_age' => 21600,
+      // filename_transliteration is not handled by migration.
+      'filename_transliteration' => FALSE,
     ],
     'system.image.gd' => [
       'jpeg_quality' => 80,