Version 1
[yaffs-website] / web / core / modules / locale / src / Tests / LocaleImportFunctionalTest.php
diff --git a/web/core/modules/locale/src/Tests/LocaleImportFunctionalTest.php b/web/core/modules/locale/src/Tests/LocaleImportFunctionalTest.php
new file mode 100644 (file)
index 0000000..c1d9a61
--- /dev/null
@@ -0,0 +1,642 @@
+<?php
+
+namespace Drupal\locale\Tests;
+
+use Drupal\simpletest\WebTestBase;
+use Drupal\Core\Language\LanguageInterface;
+
+/**
+ * Tests the import of locale files.
+ *
+ * @group locale
+ */
+class LocaleImportFunctionalTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['locale', 'dblog'];
+
+  /**
+   * A user able to create languages and import translations.
+   *
+   * @var \Drupal\user\Entity\User
+   */
+  protected $adminUser;
+
+  /**
+   * A user able to create languages, import translations and access site
+   * reports.
+   *
+   * @var \Drupal\user\Entity\User
+   */
+  protected $adminUserAccessSiteReports;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Copy test po files to the translations directory.
+    file_unmanaged_copy(__DIR__ . '/../../tests/test.de.po', 'translations://', FILE_EXISTS_REPLACE);
+    file_unmanaged_copy(__DIR__ . '/../../tests/test.xx.po', 'translations://', FILE_EXISTS_REPLACE);
+
+    $this->adminUser = $this->drupalCreateUser(['administer languages', 'translate interface', 'access administration pages']);
+    $this->adminUserAccessSiteReports = $this->drupalCreateUser(['administer languages', 'translate interface', 'access administration pages', 'access site reports']);
+    $this->drupalLogin($this->adminUser);
+
+    // Enable import of translations. By default this is disabled for automated
+    // tests.
+    $this->config('locale.settings')
+      ->set('translation.import_enabled', TRUE)
+      ->save();
+  }
+
+  /**
+   * Test import of standalone .po files.
+   */
+  public function testStandalonePoFile() {
+    // Try importing a .po file.
+    $this->importPoFile($this->getPoFile(), [
+      'langcode' => 'fr',
+    ]);
+    $this->config('locale.settings');
+    // The import should automatically create the corresponding language.
+    $this->assertRaw(t('The language %language has been created.', ['%language' => 'French']), 'The language has been automatically created.');
+
+    // The import should have created 8 strings.
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 8, '%update' => 0, '%delete' => 0]), 'The translation file was successfully imported.');
+
+    // This import should have saved plural forms to have 2 variants.
+    $locale_plurals = \Drupal::service('locale.plural.formula')->getNumberOfPlurals('fr');
+    $this->assertEqual(2, $locale_plurals, 'Plural number initialized.');
+
+    // Ensure we were redirected correctly.
+    $this->assertUrl(\Drupal::url('locale.translate_page', [], ['absolute' => TRUE]), [], 'Correct page redirection.');
+
+    // Try importing a .po file with invalid tags.
+    $this->importPoFile($this->getBadPoFile(), [
+      'langcode' => 'fr',
+    ]);
+
+    // The import should have created 1 string and rejected 2.
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 1, '%update' => 0, '%delete' => 0]), 'The translation file was successfully imported.');
+
+    $skip_message = \Drupal::translation()->formatPlural(2, 'One translation string was skipped because of disallowed or malformed HTML. <a href=":url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.', [':url' => \Drupal::url('dblog.overview')]);
+    $this->assertRaw($skip_message, 'Unsafe strings were skipped.');
+
+    // Repeat the process with a user that can access site reports, and this
+    // time the different warnings must contain links to the log.
+    $this->drupalLogin($this->adminUserAccessSiteReports);
+
+    // Try importing a .po file with invalid tags.
+    $this->importPoFile($this->getBadPoFile(), [
+      'langcode' => 'fr',
+    ]);
+
+    $skip_message = \Drupal::translation()->formatPlural(2, 'One translation string was skipped because of disallowed or malformed HTML. <a href=":url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. <a href=":url">See the log</a> for details.', [':url' => \Drupal::url('dblog.overview')]);
+    $this->assertRaw($skip_message, 'Unsafe strings were skipped.');
+
+    // Check empty files import with a user that cannot access site reports..
+    $this->drupalLogin($this->adminUser);
+    // Try importing a zero byte sized .po file.
+    $this->importPoFile($this->getEmptyPoFile(), [
+      'langcode' => 'fr',
+    ]);
+    // The import should have created 0 string and rejected 0.
+    $this->assertRaw(t('One translation file could not be imported. See the log for details.'), 'The empty translation file import reported no translations imported.');
+
+    // Repeat the process with a user that can access site reports, and this
+    // time the different warnings must contain links to the log.
+    $this->drupalLogin($this->adminUserAccessSiteReports);
+    // Try importing a zero byte sized .po file.
+    $this->importPoFile($this->getEmptyPoFile(), [
+      'langcode' => 'fr',
+    ]);
+    // The import should have created 0 string and rejected 0.
+    $this->assertRaw(t('One translation file could not be imported. <a href=":url">See the log</a> for details.', [':url' => \Drupal::url('dblog.overview')]), 'The empty translation file import reported no translations imported.');
+
+    // Try importing a .po file which doesn't exist.
+    $name = $this->randomMachineName(16);
+    $this->drupalPostForm('admin/config/regional/translate/import', [
+      'langcode' => 'fr',
+      'files[file]' => $name,
+    ], t('Import'));
+    $this->assertUrl(\Drupal::url('locale.translate_import', [], ['absolute' => TRUE]), [], 'Correct page redirection.');
+    $this->assertText(t('File to import not found.'), 'File to import not found message.');
+
+    // Try importing a .po file with overriding strings, and ensure existing
+    // strings are kept.
+    $this->importPoFile($this->getOverwritePoFile(), [
+      'langcode' => 'fr',
+    ]);
+
+    // The import should have created 1 string.
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 1, '%update' => 0, '%delete' => 0]), 'The translation file was successfully imported.');
+    // Ensure string wasn't overwritten.
+    $search = [
+      'string' => 'Montag',
+      'langcode' => 'fr',
+      'translation' => 'translated',
+    ];
+    $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
+    $this->assertText(t('No strings available.'), 'String not overwritten by imported string.');
+
+    // This import should not have changed number of plural forms.
+    $locale_plurals = \Drupal::service('locale.plural.formula')->getNumberOfPlurals('fr');
+    $this->assertEqual(2, $locale_plurals, 'Plural numbers untouched.');
+
+    // Try importing a .po file with overriding strings, and ensure existing
+    // strings are overwritten.
+    $this->importPoFile($this->getOverwritePoFile(), [
+      'langcode' => 'fr',
+      'overwrite_options[not_customized]' => TRUE,
+    ]);
+
+    // The import should have updated 2 strings.
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 0, '%update' => 2, '%delete' => 0]), 'The translation file was successfully imported.');
+    // Ensure string was overwritten.
+    $search = [
+      'string' => 'Montag',
+      'langcode' => 'fr',
+      'translation' => 'translated',
+    ];
+    $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
+    $this->assertNoText(t('No strings available.'), 'String overwritten by imported string.');
+    // This import should have changed number of plural forms.
+    $locale_plurals = \Drupal::service('locale.plural.formula')->reset()->getNumberOfPlurals('fr');
+    $this->assertEqual(3, $locale_plurals, 'Plural numbers changed.');
+
+    // Importing a .po file and mark its strings as customized strings.
+    $this->importPoFile($this->getCustomPoFile(), [
+      'langcode' => 'fr',
+      'customized' => TRUE,
+    ]);
+
+    // The import should have created 6 strings.
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 6, '%update' => 0, '%delete' => 0]), 'The customized translation file was successfully imported.');
+
+    // The database should now contain 6 customized strings (two imported
+    // strings are not translated).
+    $count = db_query('SELECT COUNT(*) FROM {locales_target} WHERE customized = :custom', [':custom' => 1])->fetchField();
+    $this->assertEqual($count, 6, 'Customized translations successfully imported.');
+
+    // Try importing a .po file with overriding strings, and ensure existing
+    // customized strings are kept.
+    $this->importPoFile($this->getCustomOverwritePoFile(), [
+      'langcode' => 'fr',
+      'overwrite_options[not_customized]' => TRUE,
+      'overwrite_options[customized]' => FALSE,
+    ]);
+
+    // The import should have created 1 string.
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 1, '%update' => 0, '%delete' => 0]), 'The customized translation file was successfully imported.');
+    // Ensure string wasn't overwritten.
+    $search = [
+      'string' => 'januari',
+      'langcode' => 'fr',
+      'translation' => 'translated',
+    ];
+    $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
+    $this->assertText(t('No strings available.'), 'Customized string not overwritten by imported string.');
+
+    // Try importing a .po file with overriding strings, and ensure existing
+    // customized strings are overwritten.
+    $this->importPoFile($this->getCustomOverwritePoFile(), [
+      'langcode' => 'fr',
+      'overwrite_options[not_customized]' => FALSE,
+      'overwrite_options[customized]' => TRUE,
+    ]);
+
+    // The import should have updated 2 strings.
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 0, '%update' => 2, '%delete' => 0]), 'The customized translation file was successfully imported.');
+    // Ensure string was overwritten.
+    $search = [
+      'string' => 'januari',
+      'langcode' => 'fr',
+      'translation' => 'translated',
+    ];
+    $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
+    $this->assertNoText(t('No strings available.'), 'Customized string overwritten by imported string.');
+
+  }
+
+  /**
+   * Test msgctxt context support.
+   */
+  public function testLanguageContext() {
+    // Try importing a .po file.
+    $this->importPoFile($this->getPoFileWithContext(), [
+      'langcode' => 'hr',
+    ]);
+
+    // We cast the return value of t() to string so as to retrieve the
+    // translated value, rendered as a string.
+    $this->assertIdentical((string) t('May', [], ['langcode' => 'hr', 'context' => 'Long month name']), 'Svibanj', 'Long month name context is working.');
+    $this->assertIdentical((string) t('May', [], ['langcode' => 'hr']), 'Svi.', 'Default context is working.');
+  }
+
+  /**
+   * Test empty msgstr at end of .po file see #611786.
+   */
+  public function testEmptyMsgstr() {
+    $langcode = 'hu';
+
+    // Try importing a .po file.
+    $this->importPoFile($this->getPoFileWithMsgstr(), [
+      'langcode' => $langcode,
+    ]);
+
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 1, '%update' => 0, '%delete' => 0]), 'The translation file was successfully imported.');
+    $this->assertIdentical((string) t('Operations', [], ['langcode' => $langcode]), 'Műveletek', 'String imported and translated.');
+
+    // Try importing a .po file.
+    $this->importPoFile($this->getPoFileWithEmptyMsgstr(), [
+      'langcode' => $langcode,
+      'overwrite_options[not_customized]' => TRUE,
+    ]);
+    $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 0, '%update' => 0, '%delete' => 1]), 'The translation file was successfully imported.');
+
+    $str = "Operations";
+    $search = [
+      'string' => $str,
+      'langcode' => $langcode,
+      'translation' => 'untranslated',
+    ];
+    $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
+    $this->assertText($str, 'Search found the string as untranslated.');
+  }
+
+  /**
+   * Tests .po file import with configuration translation.
+   */
+  public function testConfigPoFile() {
+    // Values for translations to assert. Config key, original string,
+    // translation and config property name.
+    $config_strings = [
+      'system.maintenance' => [
+        '@site is currently under maintenance. We should be back shortly. Thank you for your patience.',
+        '@site karbantartás alatt áll. Rövidesen visszatérünk. Köszönjük a türelmet.',
+        'message',
+      ],
+      'user.role.anonymous' => [
+        'Anonymous user',
+        'Névtelen felhasználó',
+        'label',
+      ],
+    ];
+
+    // Add custom language for testing.
+    $langcode = 'xx';
+    $edit = [
+      'predefined_langcode' => 'custom',
+      'langcode' => $langcode,
+      'label' => $this->randomMachineName(16),
+      'direction' => LanguageInterface::DIRECTION_LTR,
+    ];
+    $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
+
+    // Check for the source strings we are going to translate. Adding the
+    // custom language should have made the process to export configuration
+    // strings to interface translation executed.
+    $locale_storage = $this->container->get('locale.storage');
+    foreach ($config_strings as $config_string) {
+      $string = $locale_storage->findString(['source' => $config_string[0], 'context' => '', 'type' => 'configuration']);
+      $this->assertTrue($string, 'Configuration strings have been created upon installation.');
+    }
+
+    // Import a .po file to translate.
+    $this->importPoFile($this->getPoFileWithConfig(), [
+      'langcode' => $langcode,
+    ]);
+
+    // Translations got recorded in the interface translation system.
+    foreach ($config_strings as $config_string) {
+      $search = [
+        'string' => $config_string[0],
+        'langcode' => $langcode,
+        'translation' => 'all',
+      ];
+      $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
+      $this->assertText($config_string[1], format_string('Translation of @string found.', ['@string' => $config_string[0]]));
+    }
+
+    // Test that translations got recorded in the config system.
+    $overrides = \Drupal::service('language.config_factory_override');
+    foreach ($config_strings as $config_key => $config_string) {
+      $override = $overrides->getOverride($langcode, $config_key);
+      $this->assertEqual($override->get($config_string[2]), $config_string[1]);
+    }
+  }
+
+  /**
+   * Tests .po file import with user.settings configuration.
+   */
+  public function testConfigtranslationImportingPoFile() {
+    // Set the language code.
+    $langcode = 'de';
+
+    // Import a .po file to translate.
+    $this->importPoFile($this->getPoFileWithConfigDe(), [
+      'langcode' => $langcode]);
+
+    // Check that the 'Anonymous' string is translated.
+    $config = \Drupal::languageManager()->getLanguageConfigOverride($langcode, 'user.settings');
+    $this->assertEqual($config->get('anonymous'), 'Anonymous German');
+  }
+
+  /**
+   * Test the translation are imported when a new language is created.
+   */
+  public function testCreatedLanguageTranslation() {
+    // Import a .po file to add de language.
+    $this->importPoFile($this->getPoFileWithConfigDe(), ['langcode' => 'de']);
+
+    // Get the language.entity.de label and check it's been translated.
+    $override = \Drupal::languageManager()->getLanguageConfigOverride('de', 'language.entity.de');
+    $this->assertEqual($override->get('label'), 'Deutsch');
+  }
+
+  /**
+   * Helper function: import a standalone .po file in a given language.
+   *
+   * @param string $contents
+   *   Contents of the .po file to import.
+   * @param array $options
+   *   (optional) Additional options to pass to the translation import form.
+   */
+  public function importPoFile($contents, array $options = []) {
+    $name = \Drupal::service('file_system')->tempnam('temporary://', "po_") . '.po';
+    file_put_contents($name, $contents);
+    $options['files[file]'] = $name;
+    $this->drupalPostForm('admin/config/regional/translate/import', $options, t('Import'));
+    drupal_unlink($name);
+  }
+
+  /**
+   * Helper function that returns a proper .po file.
+   */
+  public function getPoFile() {
+    return <<< EOF
+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 "One sheep"
+msgid_plural "@count sheep"
+msgstr[0] "un mouton"
+msgstr[1] "@count moutons"
+
+msgid "Monday"
+msgstr "lundi"
+
+msgid "Tuesday"
+msgstr "mardi"
+
+msgid "Wednesday"
+msgstr "mercredi"
+
+msgid "Thursday"
+msgstr "jeudi"
+
+msgid "Friday"
+msgstr "vendredi"
+
+msgid "Saturday"
+msgstr "samedi"
+
+msgid "Sunday"
+msgstr "dimanche"
+EOF;
+  }
+
+  /**
+   * Helper function that returns a empty .po file.
+   */
+  public function getEmptyPoFile() {
+    return '';
+  }
+
+  /**
+   * Helper function that returns a bad .po file.
+   */
+  public function getBadPoFile() {
+    return <<< EOF
+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 "Save configuration"
+msgstr "Enregistrer la configuration"
+
+msgid "edit"
+msgstr "modifier<img SRC="javascript:alert(\'xss\');">"
+
+msgid "delete"
+msgstr "supprimer<script>alert('xss');</script>"
+
+EOF;
+  }
+
+  /**
+   * Helper function that returns a proper .po file for testing.
+   */
+  public function getOverwritePoFile() {
+    return <<< EOF
+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=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n"
+
+msgid "Monday"
+msgstr "Montag"
+
+msgid "Day"
+msgstr "Jour"
+EOF;
+  }
+
+  /**
+   * Helper function that returns a .po file which strings will be marked
+   * as customized.
+   */
+  public function getCustomPoFile() {
+    return <<< EOF
+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 "One dog"
+msgid_plural "@count dogs"
+msgstr[0] "un chien"
+msgstr[1] "@count chiens"
+
+msgid "January"
+msgstr "janvier"
+
+msgid "February"
+msgstr "février"
+
+msgid "March"
+msgstr "mars"
+
+msgid "April"
+msgstr "avril"
+
+msgid "June"
+msgstr "juin"
+EOF;
+  }
+
+  /**
+   * Helper function that returns a .po file for testing customized strings.
+   */
+  public function getCustomOverwritePoFile() {
+    return <<< EOF
+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 "januari"
+
+msgid "February"
+msgstr "februari"
+
+msgid "July"
+msgstr "juillet"
+EOF;
+  }
+
+  /**
+   * Helper function that returns a .po file with context.
+   */
+  public function getPoFileWithContext() {
+    // Croatian (code hr) is one of the languages that have a different
+    // form for the full name and the abbreviated name for the month of May.
+    return <<< EOF
+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=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n"
+
+msgctxt "Long month name"
+msgid "May"
+msgstr "Svibanj"
+
+msgid "May"
+msgstr "Svi."
+EOF;
+  }
+
+  /**
+   * Helper function that returns a .po file with an empty last item.
+   */
+  public function getPoFileWithEmptyMsgstr() {
+    return <<< EOF
+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 "Operations"
+msgstr ""
+
+EOF;
+  }
+
+  /**
+   * Helper function that returns a .po file with an empty last item.
+   */
+  public function getPoFileWithMsgstr() {
+    return <<< EOF
+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 "Operations"
+msgstr "Műveletek"
+
+msgid "Will not appear in Drupal core, so we can ensure the test passes"
+msgstr ""
+
+EOF;
+  }
+
+  /**
+   * Helper function that returns a .po file with configuration translations.
+   */
+  public function getPoFileWithConfig() {
+    return <<< EOF
+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 "@site is currently under maintenance. We should be back shortly. Thank you for your patience."
+msgstr "@site karbantartás alatt áll. Rövidesen visszatérünk. Köszönjük a türelmet."
+
+msgid "Anonymous user"
+msgstr "Névtelen felhasználó"
+
+EOF;
+  }
+
+  /**
+   * Helper function that returns a .po file with configuration translations.
+   */
+  public function getPoFileWithConfigDe() {
+    return <<< EOF
+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 "Anonymous"
+msgstr "Anonymous German"
+
+msgid "German"
+msgstr "Deutsch"
+
+EOF;
+  }
+
+}