<?php

/**
 * @file
 * Tests for locale.module.
 *
 * The test file includes:
 *  - a functional test for the language configuration forms;
 *  - functional tests for the translation functionalities, including searching;
 *  - a functional test for the PO files import feature, including validation;
 *  - functional tests for translations and templates export feature;
 *  - functional tests for the uninstall process;
 *  - a functional test for the language switching feature;
 *  - a functional test for a user's ability to change their default language;
 *  - a functional test for configuring a different path alias per language;
 *  - a functional test for configuring a different path alias per language;
 *  - a functional test for multilingual support by content type and on nodes.
 *  - a functional test for multilingual fields.
 *  - a functional test for comment language.
 *  - a functional test fot language types/negotiation info.
 */

use Drupal\simpletest\WebTestBase;

/**
 * Functional tests for JavaScript parsing for translatable strings.
 */
class LocaleJavascriptTranslationTest extends WebTestBase {
  public static function getInfo() {
    return array(
      'name' => 'Javascript translation',
      'description' => 'Tests parsing js files for translatable strings',
      'group' => 'Locale',
    );
  }

  function setUp() {
    parent::setUp('locale');
  }

  function testFileParsing() {

    $filename = drupal_get_path('module', 'locale') . '/tests/locale_test.js';

    // Parse the file to look for source strings.
    _locale_parse_js_file($filename);

    // Get all of the source strings that were found.
    $source_strings = db_select('locales_source', 's')
      ->fields('s', array('source', 'context'))
      ->condition('s.location', $filename)
      ->execute()
      ->fetchAllKeyed();

    // List of all strings that should be in the file.
    $test_strings = array(
      "Standard Call t" => '',
      "Whitespace Call t" => '',

      "Single Quote t" => '',
      "Single Quote \\'Escaped\\' t" => '',
      "Single Quote Concat strings t" => '',

      "Double Quote t" => '',
      "Double Quote \\\"Escaped\\\" t" => '',
      "Double Quote Concat strings t" => '',

      "Context !key Args t" => "Context string",

      "Context Unquoted t" => "Context string unquoted",
      "Context Single Quoted t" => "Context string single quoted",
      "Context Double Quoted t" => "Context string double quoted",

      "Standard Call plural" => '',
      "Standard Call @count plural" => '',
      "Whitespace Call plural" => '',
      "Whitespace Call @count plural" => '',

      "Single Quote plural" => '',
      "Single Quote @count plural" => '',
      "Single Quote \\'Escaped\\' plural" => '',
      "Single Quote \\'Escaped\\' @count plural" => '',

      "Double Quote plural" => '',
      "Double Quote @count plural" => '',
      "Double Quote \\\"Escaped\\\" plural" => '',
      "Double Quote \\\"Escaped\\\" @count plural" => '',

      "Context !key Args plural" => "Context string",
      "Context !key Args @count plural" => "Context string",

      "Context Unquoted plural" => "Context string unquoted",
      "Context Unquoted @count plural" => "Context string unquoted",
      "Context Single Quoted plural" => "Context string single quoted",
      "Context Single Quoted @count plural" => "Context string single quoted",
      "Context Double Quoted plural" => "Context string double quoted",
      "Context Double Quoted @count plural" => "Context string double quoted",
    );

    // Assert that all strings were found properly.
    foreach ($test_strings as $str => $context) {
      $args = array('%source' => $str, '%context' => $context);

      // Make sure that the string was found in the file.
      $this->assertTrue(isset($source_strings[$str]), t("Found source string: %source", $args));

      // Make sure that the proper context was matched.
      $this->assertTrue(isset($source_strings[$str]) && $source_strings[$str] === $context, strlen($context) > 0 ? t("Context for %source is %context", $args) : t("Context for %source is blank", $args));
    }

    $this->assertEqual(count($source_strings), count($test_strings), t("Found correct number of source strings."));
  }
}

/**
 * Functional test for string translation and validation.
 */
class LocaleTranslationFunctionalTest extends WebTestBase {
  public static function getInfo() {
    return array(
      'name' => 'String translate, search and validate',
      'description' => 'Adds a new locale and translates its name. Checks the validation of translation strings and search results.',
      'group' => 'Locale',
    );
  }

  function setUp() {
    parent::setUp('locale');
  }

  /**
   * Adds a language and tests string translation by users with the appropriate permissions.
   */
  function testStringTranslation() {
    global $base_url;

    // User to add and remove language.
    $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages'));
    // User to translate and delete string.
    $translate_user = $this->drupalCreateUser(array('translate interface', 'access administration pages'));
    // Code for the language.
    $langcode = 'xx';
    // The English name for the language. This will be translated.
    $name = $this->randomName(16);
    // This is the language indicator on the translation search screen for
    // untranslated strings.
    $language_indicator = "<em class=\"locale-untranslated\">$langcode</em> ";
    // This will be the translation of $name.
    $translation = $this->randomName(16);
    $translation_to_en = $this->randomName(16);

    // Add custom language.
    $this->drupalLogin($admin_user);
    $edit = array(
      'predefined_langcode' => 'custom',
      'langcode' => $langcode,
      'name' => $name,
      'direction' => '0',
    );
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
    // Add string.
    t($name, array(), array('langcode' => $langcode));
    // Reset locale cache.
    locale_reset();
    $this->assertRaw('"edit-site-default-' . $langcode .'"', t('Language code found.'));
    $this->assertText(t($name), t('Test language added.'));
    $this->drupalLogout();

    // Search for the name and translate it.
    $this->drupalLogin($translate_user);
    $search = array(
      'string' => $name,
      'language' => 'all',
      'translation' => 'all',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    // assertText() seems to remove the input field where $name always could be
    // found, so this is not a false assert. See how assertNoText succeeds
    // later.
    $this->assertText($name, t('Search found the name.'));
    $this->assertRaw($language_indicator, t('Name is untranslated.'));
    // Assume this is the only result, given the random name.
    $this->clickLink(t('edit'));
    // We save the lid from the path.
    $matches = array();
    preg_match('!admin/config/regional/translate/edit/(\d+)!', $this->getUrl(), $matches);
    $lid = $matches[1];
    // No t() here, it's surely not translated yet.
    $this->assertText($name, t('name found on edit screen.'));
    $this->assertNoText('English', t('No way to translate the string to English.'));
    $this->drupalLogout();
    $this->drupalLogin($admin_user);
    $this->drupalPost('admin/config/regional/language/edit/en', array('locale_translate_english' => TRUE), t('Save language'));
    $this->drupalLogout();
    $this->drupalLogin($translate_user);
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    // assertText() seems to remove the input field where $name always could be
    // found, so this is not a false assert. See how assertNoText succeeds
    // later.
    $this->assertText($name, t('Search found the name.'));
    $this->assertRaw($language_indicator, t('Name is untranslated.'));
    // Assume this is the only result, given the random name.
    $this->clickLink(t('edit'));
    $string_edit_url = $this->getUrl();
    $edit = array(
      "translations[$langcode][0]" => $translation,
      'translations[en][0]' => $translation_to_en,
    );
    $this->drupalPost(NULL, $edit, t('Save translations'));
    $this->assertText(t('The string has been saved.'), t('The string has been saved.'));
    $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.'));
    $this->drupalGet($string_edit_url);
    $this->assertRaw($translation, t('Non-English translation properly saved.'));
    $this->assertRaw($translation_to_en, t('English translation properly saved.'));
    $this->assertTrue($name != $translation && t($name, array(), array('langcode' => $langcode)) == $translation, t('t() works for non-English.'));
    // Refresh the locale() cache to get fresh data from t() below. We are in
    // the same HTTP request and therefore t() is not refreshed by saving the
    // translation above.
    locale_reset();
    // Now we should get the proper fresh translation from t().
    $this->assertTrue($name != $translation_to_en && t($name, array(), array('langcode' => 'en')) == $translation_to_en, t('t() works for English.'));
    $this->assertTrue(t($name, array(), array('langcode' => LANGUAGE_SYSTEM)) == $name, t('t() works for LANGUAGE_SYSTEM.'));
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    // The indicator should not be here.
    $this->assertNoRaw($language_indicator, t('String is translated.'));

    // Try to edit a non-existent string and ensure we're redirected correctly.
    // Assuming we don't have 999,999 strings already.
    $random_lid = 999999;
    $this->drupalGet('admin/config/regional/translate/edit/' . $random_lid);
    $this->assertText(t('String not found'), t('String not found.'));
    $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.'));
    $this->drupalLogout();

    // Delete the language.
    $this->drupalLogin($admin_user);
    $path = 'admin/config/regional/language/delete/' . $langcode;
    // This a confirm form, we do not need any fields changed.
    $this->drupalPost($path, array(), t('Delete'));
    // We need raw here because %language and %langcode will add HTML.
    $t_args = array('%language' => $name, '%langcode' => $langcode);
    $this->assertRaw(t('The %language (%langcode) language has been removed.', $t_args), t('The test language has been removed.'));
    // Reload to remove $name.
    $this->drupalGet($path);
    // Verify that language is no longer found.
    $this->assertResponse(404, t('Language no longer found.'));
    $this->drupalLogout();

    // Delete the string.
    $this->drupalLogin($translate_user);
    $search = array(
      'string' => $name,
      'language' => 'all',
      'translation' => 'all',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    // Assume this is the only result, given the random name.
    $this->clickLink(t('delete'));
    $this->assertText(t('Are you sure you want to delete the string'), t('"delete" link is correct.'));
    // Delete the string.
    $path = 'admin/config/regional/translate/delete/' . $lid;
    $this->drupalGet($path);
    // First test the 'cancel' link.
    $this->clickLink(t('Cancel'));
    $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.'));
    $this->assertRaw($name, t('The string was not deleted.'));
    // Delete the name string.
    $this->drupalPost('admin/config/regional/translate/delete/' . $lid, array(), t('Delete'));
    $this->assertText(t('The string has been removed.'), t('The string has been removed message.'));
    $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.'));
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    $this->assertNoText($name, t('Search now can not find the name.'));
  }

  /*
   * Adds a language and checks that the JavaScript translation files are
   * properly created and rebuilt on deletion.
   */
  function testJavaScriptTranslation() {
    $user = $this->drupalCreateUser(array('translate interface', 'administer languages', 'access administration pages'));
    $this->drupalLogin($user);

    $langcode = 'xx';
    // The English name for the language. This will be translated.
    $name = $this->randomName(16);

    // Add custom language.
    $edit = array(
      'predefined_langcode' => 'custom',
      'langcode' => $langcode,
      'name' => $name,
      'direction' => '0',
    );
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
    drupal_static_reset('language_list');

    // Build the JavaScript translation file.
    $this->drupalGet('admin/config/regional/translate/translate');

    // Retrieve the id of the first string available in the {locales_source}
    // table and translate it.
    $query = db_select('locales_source', 'l');
    $query->addExpression('min(l.lid)', 'lid');
    $result = $query->condition('l.location', '%.js%', 'LIKE')->execute();
    $url = 'admin/config/regional/translate/edit/' . $result->fetchObject()->lid;
    $edit = array('translations['. $langcode .'][0]' => $this->randomName());
    $this->drupalPost($url, $edit, t('Save translations'));

    // Trigger JavaScript translation parsing and building.
    _locale_rebuild_js($langcode);

    $locale_javascripts = variable_get('locale_translation_javascript', array());
    $js_file = 'public://' . variable_get('locale_js_directory', 'languages') . '/' . $langcode . '_' . $locale_javascripts[$langcode] . '.js';
    $this->assertTrue($result = file_exists($js_file), t('JavaScript file created: %file', array('%file' => $result ? $js_file : t('not found'))));

    // Test JavaScript translation rebuilding.
    file_unmanaged_delete($js_file);
    $this->assertTrue($result = !file_exists($js_file), t('JavaScript file deleted: %file', array('%file' => $result ? $js_file : t('found'))));
    cache_clear_all();
    _locale_rebuild_js($langcode);
    $this->assertTrue($result = file_exists($js_file), t('JavaScript file rebuilt: %file', array('%file' => $result ? $js_file : t('not found'))));
  }

  /**
   * Tests the validation of the translation input.
   */
  function testStringValidation() {
    global $base_url;

    // User to add language and strings.
    $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'translate interface'));
    $this->drupalLogin($admin_user);
    $langcode = 'xx';
    // The English name for the language. This will be translated.
    $name = $this->randomName(16);
    // This is the language indicator on the translation search screen for
    // untranslated strings.
    $language_indicator = "<em class=\"locale-untranslated\">$langcode</em> ";
    // These will be the invalid translations of $name.
    $key = $this->randomName(16);
    $bad_translations[$key] = "<script>alert('xss');</script>" . $key;
    $key = $this->randomName(16);
    $bad_translations[$key] = '<img SRC="javascript:alert(\'xss\');">' . $key;
    $key = $this->randomName(16);
    $bad_translations[$key] = '<<SCRIPT>alert("xss");//<</SCRIPT>' . $key;
    $key = $this->randomName(16);
    $bad_translations[$key] ="<BODY ONLOAD=alert('xss')>" . $key;

    // Add custom language.
    $edit = array(
      'predefined_langcode' => 'custom',
      'langcode' => $langcode,
      'name' => $name,
      'direction' => '0',
    );
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
    // Add string.
    t($name, array(), array('langcode' => $langcode));
    // Reset locale cache.
    $search = array(
      'string' => $name,
      'language' => 'all',
      'translation' => 'all',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    // Find the edit path.
    $content = $this->drupalGetContent();
    $this->assertTrue(preg_match('@(admin/config/regional/translate/edit/[0-9]+)@', $content, $matches), t('Found the edit path.'));
    $path = $matches[0];
    foreach ($bad_translations as $key => $translation) {
      $edit = array(
        "translations[$langcode][0]" => $translation,
      );
      $this->drupalPost($path, $edit, t('Save translations'));
      // Check for a form error on the textarea.
      $form_class = $this->xpath('//form[@id="locale-translate-edit-form"]//textarea/@class');
      $this->assertNotIdentical(FALSE, strpos($form_class[0], 'error'), t('The string was rejected as unsafe.'));
      $this->assertNoText(t('The string has been saved.'), t('The string was not saved.'));
    }
  }

  /**
   * Tests translation search form.
   */
  function testStringSearch() {
    global $base_url;

    // User to add and remove language.
    $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages'));
    // User to translate and delete string.
    $translate_user = $this->drupalCreateUser(array('translate interface', 'access administration pages'));

    // Code for the language.
    $langcode = 'xx';
    // The English name for the language. This will be translated.
    $name = $this->randomName(16);
    // This is the language indicator on the translation search screen for
    // untranslated strings.
    $language_indicator = "<em class=\"locale-untranslated\">$langcode</em> ";
    // This will be the translation of $name.
    $translation = $this->randomName(16);

    // Add custom language.
    $this->drupalLogin($admin_user);
    $edit = array(
      'predefined_langcode' => 'custom',
      'langcode' => $langcode,
      'name' => $name,
      'direction' => '0',
    );
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
    // Add string.
    t($name, array(), array('langcode' => $langcode));
    // Reset locale cache.
    locale_reset();
    $this->drupalLogout();

    // Search for the name.
    $this->drupalLogin($translate_user);
    $search = array(
      'string' => $name,
      'language' => 'all',
      'translation' => 'all',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    // assertText() seems to remove the input field where $name always could be
    // found, so this is not a false assert. See how assertNoText succeeds
    // later.
    $this->assertText($name, t('Search found the string.'));

    // Ensure untranslated string doesn't appear if searching on 'only
    // translated strings'.
    $search = array(
      'string' => $name,
      'language' => 'all',
      'translation' => 'translated',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    $this->assertText(t('No strings available.'), t("Search didn't find the string."));

    // Ensure untranslated string appears if searching on 'only untranslated
    // strings'.
    $search = array(
      'string' => $name,
      'language' => 'all',
      'translation' => 'untranslated',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    $this->assertNoText(t('No strings available.'), t('Search found the string.'));

    // Add translation.
    // Assume this is the only result, given the random name.
    $this->clickLink(t('edit'));
    // We save the lid from the path.
    $matches = array();
    preg_match('!admin/config/regional/translate/edit/(\d)+!', $this->getUrl(), $matches);
    $lid = $matches[1];
    $edit = array(
      "translations[$langcode][0]" => $translation,
    );
    $this->drupalPost(NULL, $edit, t('Save translations'));

    // Ensure translated string does appear if searching on 'only
    // translated strings'.
    $search = array(
      'string' => $translation,
      'language' => 'all',
      'translation' => 'translated',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    $this->assertNoText(t('No strings available.'), t('Search found the translation.'));

    // Ensure translated source string doesn't appear if searching on 'only
    // untranslated strings'.
    $search = array(
      'string' => $name,
      'language' => 'all',
      'translation' => 'untranslated',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    $this->assertText(t('No strings available.'), t("Search didn't find the source string."));

    // Ensure translated string doesn't appear if searching on 'only
    // untranslated strings'.
    $search = array(
      'string' => $translation,
      'language' => 'all',
      'translation' => 'untranslated',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    $this->assertText(t('No strings available.'), t("Search didn't find the translation."));

    // Ensure translated string does appear if searching on the custom language.
    $search = array(
      'string' => $translation,
      'language' => $langcode,
      'translation' => 'all',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    $this->assertNoText(t('No strings available.'), t('Search found the translation.'));

    // Ensure translated string doesn't appear if searching in System (English).
    $search = array(
      'string' => $translation,
      'language' => LANGUAGE_SYSTEM,
      'translation' => 'all',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    $this->assertText(t('No strings available.'), t("Search didn't find the translation."));

    // Search for a string that isn't in the system.
    $unavailable_string = $this->randomName(16);
    $search = array(
      'string' => $unavailable_string,
      'language' => 'all',
      'translation' => 'all',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    $this->assertText(t('No strings available.'), t("Search didn't find the invalid string."));
  }
}

/**
 * Tests plural format handling functionality.
 */
class LocalePluralFormatTest extends WebTestBase {
  public static function getInfo() {
    return array(
      'name' => 'Plural handling',
      'description' => 'Tests plural handling for various languages.',
      'group' => 'Locale',
    );
  }

  function setUp() {
    parent::setUp('locale');

    $admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages'));
    $this->drupalLogin($admin_user);
  }

  /**
   * Tests locale_get_plural() and format_plural() functionality.
   */
  function testGetPluralFormat() {
    // Import some .po files with formulas to set up the environment.
    // These will also add the languages to the system and enable them.
    $this->importPoFile($this->getPoFileWithSimplePlural(), array(
      'langcode' => 'fr',
    ));
    $this->importPoFile($this->getPoFileWithComplexPlural(), array(
      'langcode' => 'hr',
    ));

    // Attempt to import some broken .po files as well to prove that these
    // will not overwrite the proper plural formula imported above.
    $this->importPoFile($this->getPoFileWithMissingPlural(), array(
      'langcode' => 'fr',
      'overwrite_options[not_customized]' => TRUE,
    ));
    $this->importPoFile($this->getPoFileWithBrokenPlural(), array(
      'langcode' => 'hr',
      'overwrite_options[not_customized]' => TRUE,
    ));

    // Reset static caches from locale_get_plural() to ensure we get fresh data.
    drupal_static_reset('locale_get_plural');
    drupal_static_reset('locale_get_plural:plurals');
    drupal_static_reset('locale');

    // Expected plural translation strings for each plural index.
    $plural_strings = array(
      // English is not imported in this case, so we assume built-in text
      // and formulas.
      'en' => array(
        0 => '1 hour',
        1 => '@count hours',
      ),
      'fr' => array(
        0 => '1 heure',
        1 => '@count heures',
      ),
      'hr' => array(
        0 => '@count sat',
        1 => '@count sata',
        2 => '@count sati',
      ),
      // Hungarian is not imported, so it should assume the same text as
      // English, but it will always pick the plural form as per the built-in
      // logic, so only index -1 is relevant with the plural value.
      'hu' => array(
        0 => '1 hour',
        -1 => '@count hours',
      ),
    );

    // Expected plural indexes precomputed base on the plural formulas with
    // given $count value.
    $plural_tests = array(
      'en' => array(
        1 => 0,
        0 => 1,
        5 => 1,
      ),
      'fr' => array(
        1 => 0,
        0 => 0,
        5 => 1,
      ),
      'hr' => array(
        1 => 0,
        21 => 0,
        0 => 2,
        2 => 1,
        8 => 2,
      ),
      'hu' => array(
        1 => -1,
        21 => -1,
        0 => -1,
      ),
    );

    foreach ($plural_tests as $langcode => $tests) {
      foreach ($tests as $count => $expected_plural_index) {
        // Assert that the we get the right plural index.
        $this->assertIdentical(locale_get_plural($count, $langcode), $expected_plural_index, 'Computed plural index for ' . $langcode . ' for count ' . $count . ' is ' . $expected_plural_index);
        // Assert that the we get the right translation for that. Change the
        // expected index as per the logic for translation lookups.
        $expected_plural_index = ($count == 1) ? 0 : $expected_plural_index;
        $expected_plural_string = str_replace('@count', $count, $plural_strings[$langcode][$expected_plural_index]);
        $this->assertIdentical(format_plural($count, '1 hour', '@count hours', array(), array('langcode' => $langcode)), $expected_plural_string, 'Plural translation of 1 hours / @count hours for count ' . $count . ' in ' . $langcode . ' is ' . $expected_plural_string);
      }
    }
  }

  /**
   * Tests plural editing and export functionality.
   */
  function testPluralEditExport() {
    // Import some .po files with formulas to set up the environment.
    // These will also add the languages to the system and enable them.
    $this->importPoFile($this->getPoFileWithSimplePlural(), array(
      'langcode' => 'fr',
    ));
    $this->importPoFile($this->getPoFileWithComplexPlural(), array(
      'langcode' => 'hr',
    ));

    // Get the French translations.
    $this->drupalPost('admin/config/regional/translate/export', array(
      'langcode' => 'fr',
    ), t('Export'));
    // Ensure we have a translation file.
    $this->assertRaw('# French translation of Drupal', t('Exported French translation file.'));
    // Ensure our imported translations exist in the file.
    $this->assertRaw("msgid \"Monday\"\nmsgstr \"lundi\"", t('French translations present in exported file.'));
    // Check for plural export specifically.
    $this->assertRaw("msgid \"1 hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"1 heure\"\nmsgstr[1] \"@count heures\"", t('Plural translations exported properly.'));

    // Get the Croatian translations.
    $this->drupalPost('admin/config/regional/translate/export', array(
      'langcode' => 'hr',
    ), t('Export'));
    // Ensure we have a translation file.
    $this->assertRaw('# Croatian translation of Drupal', t('Exported Croatian translation file.'));
    // Ensure our imported translations exist in the file.
    $this->assertRaw("msgid \"Monday\"\nmsgstr \"Ponedjeljak\"", t('Croatian translations present in exported file.'));
    // Check for plural export specifically.
    $this->assertRaw("msgid \"1 hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"@count sat\"\nmsgstr[1] \"@count sata\"\nmsgstr[2] \"@count sati\"", t('Plural translations exported properly.'));

    // Check if the source appears on the translation page.
    $this->drupalGet('admin/config/regional/translate');
    $this->assertText("1 hour, @count hours");

    // Look up editing page for this plural string and check fields.
    $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = ''", array(':source' => "1 hour" . LOCALE_PLURAL_DELIMITER . "@count hours"))->fetchField();
    $path = 'admin/config/regional/translate/edit/' . $lid;
    $this->drupalGet($path);
    // Labels for plural editing elements.
    $this->assertText('Singular form');
    $this->assertText('First plural form');
    $this->assertText('2. plural form');
    // Plural values for both languages.
    $this->assertFieldById('edit-translations-hr-0', '@count sat');
    $this->assertFieldById('edit-translations-hr-1', '@count sata');
    $this->assertFieldById('edit-translations-hr-2', '@count sati');
    $this->assertNoFieldById('edit-translations-hr-3');
    $this->assertFieldById('edit-translations-fr-0', '1 heure');
    $this->assertFieldById('edit-translations-fr-1', '@count heures');
    $this->assertNoFieldById('edit-translations-fr-2');

    // Edit some translations and see if that took effect.
    $edit = array(
      'translations[fr][0]' => '1 heure edited',
      'translations[hr][1]' => '@count sata edited',
    );
    $this->drupalPost($path, $edit, t('Save translations'));

    // Inject a plural source string to the database. We need to use a specific
    // langcode here because the language will be English by default and will
    // not save our source string for performance optimization if we do not ask
    // specifically for a language.
    format_plural(1, '1 day', '@count days', array(), array('langcode' => 'fr'));
    // Look up editing page for this plural string and check fields.
    $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = ''", array(':source' => "1 day" . LOCALE_PLURAL_DELIMITER . "@count days"))->fetchField();
    $path = 'admin/config/regional/translate/edit/' . $lid;

    // Save complete translations for the string in both languages.
    $edit = array(
      'translations[fr][0]' => '1 jour',
      'translations[fr][1]' => '@count jours',
      'translations[hr][0]' => '@count dan',
      'translations[hr][1]' => '@count dana',
      'translations[hr][2]' => '@count dana',
    );
    $this->drupalPost($path, $edit, t('Save translations'));

    // Get the French translations.
    $this->drupalPost('admin/config/regional/translate/export', array(
      'langcode' => 'fr',
    ), t('Export'));
    // Check for plural export specifically.
    $this->assertRaw("msgid \"1 hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"1 heure edited\"\nmsgstr[1] \"@count heures\"", t('Edited French plural translations for hours exported properly.'));
    $this->assertRaw("msgid \"1 day\"\nmsgid_plural \"@count days\"\nmsgstr[0] \"1 jour\"\nmsgstr[1] \"@count jours\"", t('Added French plural translations for days exported properly.'));

    // Get the Croatian translations.
    $this->drupalPost('admin/config/regional/translate/export', array(
      'langcode' => 'hr',
    ), t('Export'));
    // Check for plural export specifically.
    $this->assertRaw("msgid \"1 hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"@count sat\"\nmsgstr[1] \"@count sata edited\"\nmsgstr[2] \"@count sati\"", t('Edited Croatian plural translations exported properly.'));
    $this->assertRaw("msgid \"1 day\"\nmsgid_plural \"@count days\"\nmsgstr[0] \"@count dan\"\nmsgstr[1] \"@count dana\"\nmsgstr[2] \"@count dana\"", t('Added Croatian plural translations exported properly.'));
  }


  /**
   * Imports a standalone .po file in a given language.
   *
   * @param $contents
   *   Contents of the .po file to import.
   * @param $options
   *   Additional options to pass to the translation import form.
   */
  function importPoFile($contents, array $options = array()) {
    $name = tempnam('temporary://', "po_") . '.po';
    file_put_contents($name, $contents);
    $options['files[file]'] = $name;
    $this->drupalPost('admin/config/regional/translate/import', $options, t('Import'));
    drupal_unlink($name);
  }

  /**
   * Returns a .po file with a simple plural formula.
   */
  function getPoFileWithSimplePlural() {
    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 "1 hour"
msgid_plural "@count hours"
msgstr[0] "1 heure"
msgstr[1] "@count heures"

msgid "Monday"
msgstr "lundi"
EOF;
  }

  /**
   * Returns a .po file with a complex plural formula.
   */
  function getPoFileWithComplexPlural() {
    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 "1 hour"
msgid_plural "@count hours"
msgstr[0] "@count sat"
msgstr[1] "@count sata"
msgstr[2] "@count sati"

msgid "Monday"
msgstr "Ponedjeljak"
EOF;
  }

  /**
   * Returns a .po file with a missing plural formula.
   */
  function getPoFileWithMissingPlural() {
    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"

msgid "Monday"
msgstr "lundi"
EOF;
  }

  /**
   * Returns a .po file with a broken plural formula.
   */
  function getPoFileWithBrokenPlural() {
    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: broken, will not parse\\n"

msgid "Monday"
msgstr "Ponedjeljak"
EOF;
  }
}

/**
 * Functional tests for the import of translation files.
 */
class LocaleImportFunctionalTest extends WebTestBase {
  public static function getInfo() {
    return array(
      'name' => 'Translation import',
      'description' => 'Tests the import of locale files.',
      'group' => 'Locale',
    );
  }

  /**
   * A user able to create languages and import translations.
   */
  protected $admin_user = NULL;

  function setUp() {
    parent::setUp(array('locale', 'dblog'));

    // Set the translation file directory.
    variable_set('locale_translate_file_directory', drupal_get_path('module', 'locale') . '/tests');

    $this->admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages'));
    $this->drupalLogin($this->admin_user);
  }

  /**
   * Test import of standalone .po files.
   */
  function testStandalonePoFile() {
    // Try importing a .po file.
    $this->importPoFile($this->getPoFile(), array(
      'langcode' => 'fr',
    ));

    // The import should automatically create the corresponding language.
    $this->assertRaw(t('The language %language has been created.', array('%language' => 'French')), t('The language has been automatically created.'));

    // The import should have created 8 strings.
    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 8, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));

    // This import should have saved plural forms to have 2 variants.
    $locale_plurals = variable_get('locale_translation_plurals', array());
    $this->assert($locale_plurals['fr']['plurals'] == 2, t('Plural number initialized.'));

    // Ensure we were redirected correctly.
    $this->assertEqual($this->getUrl(), url('admin/config/regional/translate', array('absolute' => TRUE)), t('Correct page redirection.'));


    // Try importing a .po file with invalid tags.
    $this->importPoFile($this->getBadPoFile(), array(
      'langcode' => 'fr',
    ));

    // The import should have created 1 string and rejected 2.
    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
    $skip_message = format_plural(2, 'A 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.', array('@url' => url('admin/reports/dblog')));
    $this->assertRaw($skip_message, t('Unsafe strings were skipped.'));


    // Try importing a .po file which doesn't exist.
    $name = $this->randomName(16);
    $this->drupalPost('admin/config/regional/translate/import', array(
      'langcode' => 'fr',
      'files[file]' => $name,
    ), t('Import'));
    $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/import', array('absolute' => TRUE)), t('Correct page redirection.'));
    $this->assertText(t('File to import not found.'), t('File to import not found message.'));


    // Try importing a .po file with overriding strings, and ensure existing
    // strings are kept.
    $this->importPoFile($this->getOverwritePoFile(), array(
      'langcode' => 'fr',
    ));

    // The import should have created 1 string.
    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
    // Ensure string wasn't overwritten.
    $search = array(
      'string' => 'Montag',
      'language' => 'fr',
      'translation' => 'translated',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    $this->assertText(t('No strings available.'), t('String not overwritten by imported string.'));

    // This import should not have changed number of plural forms.
    $locale_plurals = variable_get('locale_translation_plurals', array());
    $this->assert($locale_plurals['fr']['plurals'] == 2, t('Plural numbers untouched.'));

    // Try importing a .po file with overriding strings, and ensure existing
    // strings are overwritten.
    $this->importPoFile($this->getOverwritePoFile(), array(
      'langcode' => 'fr',
      'overwrite_options[not_customized]' => TRUE,
    ));

    // The import should have updated 2 strings.
    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The translation file was successfully imported.'));
    // Ensure string was overwritten.
    $search = array(
      'string' => 'Montag',
      'language' => 'fr',
      'translation' => 'translated',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    $this->assertNoText(t('No strings available.'), t('String overwritten by imported string.'));
    // This import should have changed number of plural forms.
    $locale_plurals = variable_get('locale_translation_plurals', array());
    $this->assert($locale_plurals['fr']['plurals'] == 3, t('Plural numbers changed.'));

    // Importing a .po file and mark its strings as customized strings.
    $this->importPoFile($this->getCustomPoFile(), array(
      'langcode' => 'fr',
      'customized' => TRUE,
    ));

    // The import should have created 6 strings.
    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 6, '%update' => 0, '%delete' => 0)), t('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 lid FROM {locales_target} WHERE customized = :custom', array(':custom' => 1))->rowCount();
    $this->assertEqual($count, 6, t('Customized translations succesfully imported.'));

    // Try importing a .po file with overriding strings, and ensure existing
    // customized strings are kept.
    $this->importPoFile($this->getCustomOverwritePoFile(), array(
      'langcode' => 'fr',
      'overwrite_options[not_customized]' => TRUE,
      'overwrite_options[customized]' => FALSE,
    ));

    // The import should have created 1 string.
    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The customized translation file was successfully imported.'));
    // Ensure string wasn't overwritten.
    $search = array(
      'string' => 'januari',
      'language' => 'fr',
      'translation' => 'translated',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    $this->assertText(t('No strings available.'), t('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(), array(
      'langcode' => 'fr',
      'overwrite_options[not_customized]' => FALSE,
      'overwrite_options[customized]' => TRUE,
    ));

    // The import should have updated 2 strings.
    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The customized translation file was successfully imported.'));
    // Ensure string was overwritten.
    $search = array(
      'string' => 'januari',
      'language' => 'fr',
      'translation' => 'translated',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    $this->assertNoText(t('No strings available.'), t('Customized string overwritten by imported string.'));

  }

  /**
   * Test automatic import of a module's translation files.
   */
  function testAutomaticModuleTranslationImportLanguageEnable() {
    // Code for the language - manually set to match the test translation file.
    $langcode = 'xx';
    // The English name for the language.
    $name = $this->randomName(16);

    // Create a custom language.
    $edit = array(
      'predefined_langcode' => 'custom',
      'langcode' => $langcode,
      'name' => $name,
      'direction' => '0',
    );
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));

    // Ensure the translation file was automatically imported when language was
    // added.
    $this->assertText(t('One translation file imported.'), t('Language file automatically imported.'));

    // Ensure strings were successfully imported.
    $search = array(
      'string' => 'lundi',
      'language' => $langcode,
      'translation' => 'translated',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    $this->assertNoText(t('No strings available.'), t('String successfully imported.'));
  }

  /**
   * Test msgctxt context support.
   */
  function testLanguageContext() {
    // Try importing a .po file.
    $this->importPoFile($this->getPoFileWithContext(), array(
      'langcode' => 'hr',
    ));

    $this->assertIdentical(t('May', array(), array('langcode' => 'hr', 'context' => 'Long month name')), 'Svibanj', t('Long month name context is working.'));
    $this->assertIdentical(t('May', array(), array('langcode' => 'hr')), 'Svi.', t('Default context is working.'));
  }

  /**
   * Test empty msgstr at end of .po file see #611786.
   */
  function testEmptyMsgstr() {
    $langcode = 'hu';

    // Try importing a .po file.
    $this->importPoFile($this->getPoFileWithMsgstr(), array(
      'langcode' => $langcode,
    ));

    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 1, '%update' => 0, '%delete' => 0)), t('The translation file was successfully imported.'));
    $this->assertIdentical(t('Operations', array(), array('langcode' => $langcode)), 'Műveletek', t('String imported and translated.'));

    // Try importing a .po file.
    $this->importPoFile($this->getPoFileWithEmptyMsgstr(), array(
      'langcode' => $langcode,
      'overwrite_options[not_customized]' => TRUE,
    ));
    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 0, '%delete' => 1)), t('The translation file was successfully imported.'));
    // This is the language indicator on the translation search screen for
    // untranslated strings.
    $language_indicator = "<em class=\"locale-untranslated\">$langcode</em> ";
    $str = "Operations";
    $search = array(
      'string' => $str,
      'language' => 'all',
      'translation' => 'all',
    );
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
    // assertText() seems to remove the input field where $str always could be
    // found, so this is not a false assert.
    $this->assertText($str, t('Search found the string.'));
    $this->assertRaw($language_indicator, t('String is untranslated again.'));
  }

  /**
   * Helper function: import a standalone .po file in a given language.
   *
   * @param $contents
   *   Contents of the .po file to import.
   * @param $options
   *   Additional options to pass to the translation import form.
   */
  function importPoFile($contents, array $options = array()) {
    $name = tempnam('temporary://', "po_") . '.po';
    file_put_contents($name, $contents);
    $options['files[file]'] = $name;
    $this->drupalPost('admin/config/regional/translate/import', $options, t('Import'));
    drupal_unlink($name);
  }

  /**
   * Helper function that returns a proper .po file.
   */
  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 bad .po file.
   */
  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.
   */
  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.
   */
  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.
   */
  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.
   */
  function getPoFileWithContext() {
    // Croatian (code hr) is one the the languages that have a different
    // form for the full name and the abbreviated name for the month 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.
   */
  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.
   */
  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;
  }

}

/**
 * Functional tests for the export of translation files.
 */
class LocaleExportFunctionalTest extends WebTestBase {
  public static function getInfo() {
    return array(
      'name' => 'Translation export',
      'description' => 'Tests the exportation of locale files.',
      'group' => 'Locale',
    );
  }

  /**
   * A user able to create languages and export translations.
   */
  protected $admin_user = NULL;

  function setUp() {
    parent::setUp('locale');

    $this->admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages'));
    $this->drupalLogin($this->admin_user);
  }

  /**
   * Test exportation of translations.
   */
  function testExportTranslation() {
    // First import some known translations.
    // This will also automatically enable the 'fr' language.
    $name = tempnam('temporary://', "po_") . '.po';
    file_put_contents($name, $this->getPoFile());
    $this->drupalPost('admin/config/regional/translate/import', array(
      'langcode' => 'fr',
      'files[file]' => $name,
    ), t('Import'));
    drupal_unlink($name);

    // Get the French translations.
    $this->drupalPost('admin/config/regional/translate/export', array(
      'langcode' => 'fr',
    ), t('Export'));

    // Ensure we have a translation file.
    $this->assertRaw('# French translation of Drupal', t('Exported French translation file.'));
    // Ensure our imported translations exist in the file.
    $this->assertRaw('msgstr "lundi"', t('French translations present in exported file.'));

    // Import some more French translations which will be marked as customized.
    $name = tempnam('temporary://', "po2_") . '.po';
    file_put_contents($name, $this->getCustomPoFile());
    $this->drupalPost('admin/config/regional/translate/import', array(
      'langcode' => 'fr',
      'files[file]' => $name,
      'customized' => 1,
    ), t('Import'));
    drupal_unlink($name);

    // We can't import a string with an empty translation, but calling
    // locale() for an new string creates an entry in the locales_source table.
    locale('February', NULL, 'fr');

    // Export only customized French translations.
    $this->drupalPost('admin/config/regional/translate/export', array(
      'langcode' => 'fr',
      'content_options[not_customized]' => FALSE,
      'content_options[customized]' => TRUE,
      'content_options[not_translated]' => FALSE,
    ), t('Export'));

    // Ensure we have a translation file.
    $this->assertRaw('# French translation of Drupal', t('Exported French translation file with only customized strings.'));
    // Ensure the customized translations exist in the file.
    $this->assertRaw('msgstr "janvier"', t('French custom translation present in exported file.'));
    // Ensure no untranslated strings exist in the file.
    $this->assertNoRaw('msgid "February"', t('Untranslated string not present in exported file.'));

    // Export only untranslated French translations.
    $this->drupalPost('admin/config/regional/translate/export', array(
      'langcode' => 'fr',
      'content_options[not_customized]' => FALSE,
      'content_options[customized]' => FALSE,
      'content_options[not_translated]' => TRUE,
    ), t('Export'));

    // Ensure we have a translation file.
    $this->assertRaw('# French translation of Drupal', t('Exported French translation file with only untranslated strings.'));
    // Ensure no customized translations exist in the file.
    $this->assertNoRaw('msgstr "janvier"', t('French custom translation not present in exported file.'));
    // Ensure the untranslated strings exist in the file.
    $this->assertRaw('msgid "February"', t('Untranslated string present in exported file.'));
  }

  /**
   * Test exportation of translation template file.
   */
  function testExportTranslationTemplateFile() {
    // Get the translation template file.
    $this->drupalPost('admin/config/regional/translate/export', array(), t('Export'));
    // Ensure we have a translation file.
    $this->assertRaw('# LANGUAGE translation of PROJECT', t('Exported translation template file.'));
  }

  /**
   * Helper function that returns a proper .po file.
   */
  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 "Monday"
msgstr "lundi"
EOF;
  }

  /**
   * Helper function that returns a .po file which strings will be marked
   * as customized.
   */
  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 "January"
msgstr "janvier"
EOF;
  }

}

/**
 * Tests for the st() function.
 */
class LocaleInstallTest extends WebTestBase {
  public static function getInfo() {
    return array(
      'name' => 'String translation using st()',
      'description' => 'Tests that st() works like t().',
      'group' => 'Locale',
    );
  }

  function setUp() {
    parent::setUp('locale');

    // st() lives in install.inc, so ensure that it is loaded for all tests.
    require_once DRUPAL_ROOT . '/core/includes/install.inc';
  }

  /**
   * Verify that function signatures of t() and st() are equal.
   */
  function testFunctionSignatures() {
    $reflector_t = new ReflectionFunction('t');
    $reflector_st = new ReflectionFunction('st');
    $this->assertEqual($reflector_t->getParameters(), $reflector_st->getParameters(), t('Function signatures of t() and st() are equal.'));
  }
}

/**
 * Locale uninstall with English UI functional test.
 */
class LocaleUninstallFunctionalTest extends WebTestBase {
  public static function getInfo() {
    return array(
      'name' => 'Locale uninstall (EN)',
      'description' => 'Tests the uninstall process using the built-in UI language.',
      'group' => 'Locale',
    );
  }

  /**
   * The default language set for the UI before uninstall.
   */
  protected $language;

  function setUp() {
    parent::setUp(array('node', 'locale'));
    $this->langcode = 'en';

    // Create Article node type.
    $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
  }

  /**
   * Check if the values of the Locale variables are correct after uninstall.
   */
  function testUninstallProcess() {
    $locale_module = array('locale', 'language');

    $language = (object) array(
      'langcode' => 'fr',
      'name' => 'French',
      'default' => $this->langcode == 'fr',
    );
    language_save($language);

    // Check the UI language.
    drupal_language_initialize();
    $this->assertEqual(drupal_container()->get(LANGUAGE_TYPE_INTERFACE)->langcode, $this->langcode, t('Current language: %lang', array('%lang' => drupal_container()->get(LANGUAGE_TYPE_INTERFACE)->langcode)));

    // Enable multilingual workflow option for articles.
    variable_set('node_type_language_article', 1);

    // Change JavaScript translations directory.
    variable_set('locale_js_directory', 'js_translations');

    // Build the JavaScript translation file for French.
    $user = $this->drupalCreateUser(array('translate interface', 'access administration pages'));
    $this->drupalLogin($user);
    $this->drupalGet('admin/config/regional/translate/translate');
    $string = db_query('SELECT min(lid) AS lid FROM {locales_source} WHERE location LIKE :location', array(
      ':location' => '%.js%',
    ))->fetchObject();
    $edit = array('translations[fr][0]' => 'french translation');
    $this->drupalPost('admin/config/regional/translate/edit/' . $string->lid, $edit, t('Save translations'));
    _locale_rebuild_js('fr');
    $locale_javascripts = variable_get('locale_translation_javascript', array());
    $js_file = 'public://' . variable_get('locale_js_directory', 'languages') . '/fr_' . $locale_javascripts['fr'] . '.js';
    $this->assertTrue($result = file_exists($js_file), t('JavaScript file created: %file', array('%file' => $result ? $js_file : t('none'))));

    // Disable string caching.
    variable_set('locale_cache_strings', 0);

    // Change language negotiation options.
    drupal_load('module', 'locale');
    variable_set('language_types', language_types_get_default() + array('language_custom' => TRUE));
    variable_set('language_negotiation_' . LANGUAGE_TYPE_INTERFACE, language_language_negotiation_info());
    variable_set('language_negotiation_' . LANGUAGE_TYPE_CONTENT, language_language_negotiation_info());
    variable_set('language_negotiation_' . LANGUAGE_TYPE_URL, language_language_negotiation_info());

    // Change language negotiation settings.
    variable_set('language_negotiation_url_part', LANGUAGE_NEGOTIATION_URL_PREFIX);
    variable_set('language_negotiation_session_param', TRUE);

    // Uninstall Locale.
    module_disable($locale_module);
    drupal_uninstall_modules($locale_module);

    // Visit the front page.
    $this->drupalGet('');

    // Check the init language logic.
    drupal_language_initialize();
    $this->assertEqual(drupal_container()->get(LANGUAGE_TYPE_INTERFACE)->langcode, 'en', t('Language after uninstall: %lang', array('%lang' => drupal_container()->get(LANGUAGE_TYPE_INTERFACE)->langcode)));

    // Check JavaScript files deletion.
    $this->assertTrue($result = !file_exists($js_file), t('JavaScript file deleted: %file', array('%file' => $result ? $js_file : t('found'))));

    // Check language count.
    $language_count = variable_get('language_count', 1);
    $this->assertEqual($language_count, 1, t('Language count: %count', array('%count' => $language_count)));

    // Check language negotiation.
    require_once DRUPAL_ROOT . '/core/includes/language.inc';
    $this->assertTrue(count(language_types_get_all()) == count(language_types_get_default()), t('Language types reset'));
    $language_negotiation = language_negotiation_method_get_first(LANGUAGE_TYPE_INTERFACE) == LANGUAGE_NEGOTIATION_DEFAULT;
    $this->assertTrue($language_negotiation, t('Interface language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set'))));
    $language_negotiation = language_negotiation_method_get_first(LANGUAGE_TYPE_CONTENT) == LANGUAGE_NEGOTIATION_DEFAULT;
    $this->assertTrue($language_negotiation, t('Content language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set'))));
    $language_negotiation = language_negotiation_method_get_first(LANGUAGE_TYPE_URL) == LANGUAGE_NEGOTIATION_DEFAULT;
    $this->assertTrue($language_negotiation, t('URL language negotiation: %setting', array('%setting' => t($language_negotiation ? 'none' : 'set'))));

    // Check language negotiation method settings.
    $this->assertFalse(variable_get('language_negotiation_url_part', FALSE), t('URL language negotiation method indicator settings cleared.'));
    $this->assertFalse(variable_get('language_negotiation_session_param', FALSE), t('Visit language negotiation method settings cleared.'));

    // Check JavaScript parsed.
    $javascript_parsed_count = count(variable_get('javascript_parsed', array()));
    $this->assertEqual($javascript_parsed_count, 0, t('JavaScript parsed count: %count', array('%count' => $javascript_parsed_count)));

    // Check JavaScript translations directory.
    $locale_js_directory = variable_get('locale_js_directory', 'languages');
    $this->assertEqual($locale_js_directory, 'languages', t('JavaScript translations directory: %dir', array('%dir' => $locale_js_directory)));

    // Check string caching.
    $locale_cache_strings = variable_get('locale_cache_strings', 1);
    $this->assertEqual($locale_cache_strings, 1, t('String caching: %status', array('%status' => t($locale_cache_strings ? 'enabled': 'disabled'))));
  }
}

/**
 * Locale uninstall with French UI functional test.
 *
 * Because this class extends LocaleUninstallFunctionalTest, it doesn't require a new
 * test of its own. Rather, it switches the default UI language in setUp and then
 * runs the testUninstallProcess (which it inherits from LocaleUninstallFunctionalTest)
 * to test with this new language.
 */
class LocaleUninstallFrenchFunctionalTest extends LocaleUninstallFunctionalTest {
  public static function getInfo() {
    return array(
      'name' => 'Locale uninstall (FR)',
      'description' => 'Tests the uninstall process using French as interface language.',
      'group' => 'Locale',
    );
  }

  function setUp() {
    parent::setUp();
    $this->langcode = 'fr';
  }
}

/**
 * Functional tests for configuring a different path alias per language.
 */
class LocalePathFunctionalTest extends WebTestBase {
  public static function getInfo() {
    return array(
      'name' => 'Path language settings',
      'description' => 'Checks you can configure a language for individual url aliases.',
      'group' => 'Locale',
    );
  }

  function setUp() {
    parent::setUp(array('node', 'locale', 'path'));

    $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
    variable_set('site_frontpage', 'node');
  }

  /**
   * Test if a language can be associated with a path alias.
   */
  function testPathLanguageConfiguration() {
    global $base_url;

    // User to add and remove language.
    $admin_user = $this->drupalCreateUser(array('administer languages', 'create page content', 'administer url aliases', 'create url aliases', 'access administration pages'));

    // Add custom language.
    $this->drupalLogin($admin_user);
    // Code for the language.
    $langcode = 'xx';
    // The English name for the language.
    $name = $this->randomName(16);
    // The domain prefix.
    $prefix = $langcode;
    $edit = array(
      'predefined_langcode' => 'custom',
      'langcode' => $langcode,
      'name' => $name,
      'direction' => '0',
    );
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));

    // Set path prefix.
    $edit = array( "prefix[$langcode]" => $prefix );
    $this->drupalPost('admin/config/regional/language/detection/url', $edit, t('Save configuration'));

    // Check that the "xx" front page is readily available because path prefix
    // negotiation is pre-configured.
    $this->drupalGet($prefix);
    $this->assertText(t('Welcome to Drupal'), t('The "xx" front page is readibly available.'));

    // Create a node.
    $node = $this->drupalCreateNode(array('type' => 'page'));

    // Create a path alias in default language (English).
    $path = 'admin/config/search/path/add';
    $english_path = $this->randomName(8);
    $edit = array(
      'source'   => 'node/' . $node->nid,
      'alias'    => $english_path,
      'langcode' => 'en',
    );
    $this->drupalPost($path, $edit, t('Save'));

    // Create a path alias in new custom language.
    $custom_language_path = $this->randomName(8);
    $edit = array(
      'source'   => 'node/' . $node->nid,
      'alias'    => $custom_language_path,
      'langcode' => $langcode,
    );
    $this->drupalPost($path, $edit, t('Save'));

    // Confirm English language path alias works.
    $this->drupalGet($english_path);
    $this->assertText($node->title, t('English alias works.'));

    // Confirm custom language path alias works.
    $this->drupalGet($prefix . '/' . $custom_language_path);
    $this->assertText($node->title, t('Custom language alias works.'));

    // Create a custom path.
    $custom_path = $this->randomName(8);

    // Check priority of language for alias by source path.
    $edit = array(
      'source'   => 'node/' . $node->nid,
      'alias'    => $custom_path,
      'langcode' => LANGUAGE_NOT_SPECIFIED,
    );
    path_save($edit);
    $lookup_path = drupal_lookup_path('alias', 'node/' . $node->nid, 'en');
    $this->assertEqual($english_path, $lookup_path, t('English language alias has priority.'));
    // Same check for language 'xx'.
    $lookup_path = drupal_lookup_path('alias', 'node/' . $node->nid, $prefix);
    $this->assertEqual($custom_language_path, $lookup_path, t('Custom language alias has priority.'));
    path_delete($edit);

    // Create language nodes to check priority of aliases.
    $first_node = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1));
    $second_node = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1));

    // Assign a custom path alias to the first node with the English language.
    $edit = array(
      'source'   => 'node/' . $first_node->nid,
      'alias'    => $custom_path,
      'langcode' => 'en',
    );
    path_save($edit);

    // Assign a custom path alias to second node with LANGUAGE_NOT_SPECIFIED.
    $edit = array(
      'source'   => 'node/' . $second_node->nid,
      'alias'    => $custom_path,
      'langcode' => LANGUAGE_NOT_SPECIFIED,
    );
    path_save($edit);

    // Test that both node titles link to our path alias.
    $this->drupalGet('<front>');
    $custom_path_url = base_path() . $GLOBALS['script_path'] . $custom_path;
    $elements = $this->xpath('//a[@href=:href and .=:title]', array(':href' => $custom_path_url, ':title' => $first_node->title));
    $this->assertTrue(!empty($elements), t('First node links to the path alias.'));
    $elements = $this->xpath('//a[@href=:href and .=:title]', array(':href' => $custom_path_url, ':title' => $second_node->title));
    $this->assertTrue(!empty($elements), t('Second node links to the path alias.'));

    // Confirm that the custom path leads to the first node.
    $this->drupalGet($custom_path);
    $this->assertText($first_node->title, t('Custom alias returns first node.'));

    // Confirm that the custom path with prefix leads to the second node.
    $this->drupalGet($prefix . '/' . $custom_path);
    $this->assertText($second_node->title, t('Custom alias with prefix returns second node.'));

  }
}

/**
 * Functional tests for multilingual support on nodes.
 */
class LocaleContentFunctionalTest extends WebTestBase {
  protected $profile = 'standard';

  public static function getInfo() {
    return array(
      'name' => 'Content language settings',
      'description' => 'Checks you can enable multilingual support on content types and configure a language for a node.',
      'group' => 'Locale',
    );
  }

  function setUp() {
    parent::setUp('locale');
  }

  /**
   * Verifies that machine name fields are always LTR.
   */
  function testMachineNameLTR() {
    // User to add and remove language.
    $admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages'));

    // Log in as admin.
    $this->drupalLogin($admin_user);

    // Verify that the machine name field is LTR for a new content type.
    $this->drupalGet('admin/structure/types/add');
    $this->assertFieldByXpath('//input[@name="type" and @dir="ltr"]', NULL, 'The machine name field is LTR when no additional language is configured.');

    // Install the Arabic language (which is RTL) and configure as the default.
    $edit = array();
    $edit['predefined_langcode'] = 'ar';
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));

    $edit = array();
    $edit['site_default'] = 'ar';
    $this->drupalPost(NULL, $edit, t('Save configuration'));

    // Verify that the machine name field is still LTR for a new content type.
    $this->drupalGet('admin/structure/types/add');
    $this->assertFieldByXpath('//input[@name="type" and @dir="ltr"]', NULL, 'The machine name field is LTR when the default language is RTL.');
  }

  /**
   * Test if a content type can be set to multilingual and language is present.
   */
  function testContentTypeLanguageConfiguration() {
    global $base_url;

    // User to add and remove language.
    $admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages'));
    // User to create a node.
    $web_user = $this->drupalCreateUser(array('create article content', 'create page content', 'edit any page content'));

    // Add custom language.
    $this->drupalLogin($admin_user);
    // Code for the language.
    $langcode = 'xx';
    // The English name for the language.
    $name = $this->randomName(16);
    $edit = array(
      'predefined_langcode' => 'custom',
      'langcode' => $langcode,
      'name' => $name,
      'direction' => '0',
    );
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));

    // Set "Basic page" content type to use multilingual support.
    $this->drupalGet('admin/structure/types/manage/page');
    $this->assertText(t('Multilingual support'), t('Multilingual support fieldset present on content type configuration form.'));
    $edit = array(
      'node_type_language' => 1,
    );
    $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type'));
    $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), t('Basic page content type has been updated.'));
    $this->drupalLogout();

    // Verify language selection is not present on add article form.
    $this->drupalLogin($web_user);
    $this->drupalGet('node/add/article');
    // Verify language select list is not present.
    $this->assertNoFieldByName('language', NULL, t('Language select not present on add article form.'));

    // Verify language selection appears on add "Basic page" form.
    $this->drupalGet('node/add/page');
    // Verify language select list is present.
    $this->assertFieldByName('langcode', NULL, t('Language select present on add Basic page form.'));
    // Ensure language appears.
    $this->assertText($name, t('Language present.'));

    // Create "Basic page" content.
    $node_title = $this->randomName();
    $node_body =  $this->randomName();
    $edit = array(
      'type' => 'page',
      'title' => $node_title,
      'body' => array($langcode => array(array('value' => $node_body))),
      'langcode' => $langcode,
    );
    $node = $this->drupalCreateNode($edit);
    // Edit the content and ensure correct language is selected.
    $path = 'node/' . $node->nid . '/edit';
    $this->drupalGet($path);
    $this->assertRaw('<option value="' . $langcode . '" selected="selected">' .  $name . '</option>', t('Correct language selected.'));
    // Ensure we can change the node language.
    $edit = array(
      'langcode' => 'en',
    );
    $this->drupalPost($path, $edit, t('Save'));
    $this->assertRaw(t('%title has been updated.', array('%title' => $node_title)), t('Basic page content updated.'));

    $this->drupalLogout();
  }

  /**
   * Test if a dir and lang tags exist in node's attributes.
   */
  function testContentTypeDirLang() {
    // User to add and remove language.
    $admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages'));
    // User to create a node.
    $web_user = $this->drupalCreateUser(array('create article content', 'edit own article content'));

    // Login as admin.
    $this->drupalLogin($admin_user);

    // Install Arabic language.
    $edit = array();
    $edit['predefined_langcode'] = 'ar';
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));

    // Install Spanish language.
    $edit = array();
    $edit['predefined_langcode'] = 'es';
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));

    // Set "Article" content type to use multilingual support.
    $this->drupalGet('admin/structure/types/manage/article');
    $edit = array(
      'node_type_language' => 1,
    );
    $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
    $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Article')), t('Article content type has been updated.'));
    $this->drupalLogout();

    // Login as web user to add new article.
    $this->drupalLogin($web_user);

    // Create three nodes: English, Arabic and Spanish.
    $node_en = $this->createNodeArticle('en');
    $node_ar = $this->createNodeArticle('ar');
    $node_es = $this->createNodeArticle('es');

    $this->drupalGet('node');

    // Check if English node does not have lang tag.
    $pattern = '|id="node-' . $node_en->nid . '"[^<>]*lang="en"|';
    $this->assertNoPattern($pattern, t('The lang tag has not been assigned to the English node.'));

    // Check if English node does not have dir tag.
    $pattern = '|id="node-' . $node_en->nid . '"[^<>]*dir="ltr"|';
    $this->assertNoPattern($pattern, t('The dir tag has not been assigned to the English node.'));

    // Check if Arabic node has lang="ar" & dir="rtl" tags.
    $pattern = '|id="node-' . $node_ar->nid . '"[^<>]*lang="ar" dir="rtl"|';
    $this->assertPattern($pattern, t('The lang and dir tags have been assigned correctly to the Arabic node.'));

    // Check if Spanish node has lang="es" tag.
    $pattern = '|id="node-' . $node_es->nid . '"[^<>]*lang="es"|';
    $this->assertPattern($pattern, t('The lang tag has been assigned correctly to the Spanish node.'));

    // Check if Spanish node does not have dir="ltr" tag.
    $pattern = '|id="node-' . $node_es->nid . '"[^<>]*lang="es" dir="ltr"|';
    $this->assertNoPattern($pattern, t('The dir tag has not been assigned to the Spanish node.'));

    $this->drupalLogout();
  }

  /**
   * Create node in a specific language.
   */
  protected function createNodeArticle($langcode) {
    $this->drupalGet('node/add/article');
    $node_title = $this->randomName();
    $node_body =  $this->randomName();
    $edit = array(
      'type' => 'article',
      'title' => $node_title,
      'body' => array($langcode => array(array('value' => $node_body))),
      'langcode' => $langcode,
      'promote' => 1,
    );
    return $this->drupalCreateNode($edit);
  }
}

/**
 * Functional test for multilingual fields.
 */
class LocaleMultilingualFieldsFunctionalTest extends WebTestBase {
  public static function getInfo() {
    return array(
      'name' => 'Multilingual fields',
      'description' => 'Test multilingual support for fields.',
      'group' => 'Locale',
    );
  }

  function setUp() {
    parent::setUp(array('node', 'locale'));

    // Create Basic page node type.
    $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));

    // Setup users.
    $admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages', 'create page content', 'edit own page content'));
    $this->drupalLogin($admin_user);

    // Add a new language.
    $language = (object) array(
      'langcode' => 'it',
      'name' => 'Italian',
    );
    language_save($language);

    // Enable URL language detection and selection.
    $edit = array('language_interface[enabled][language-url]' => '1');
    $this->drupalPost('admin/config/regional/language/detection', $edit, t('Save settings'));

    // Set "Basic page" content type to use multilingual support.
    $edit = array(
      'node_type_language' => 1,
    );
    $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type'));
    $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), t('Basic page content type has been updated.'));

    // Make node body translatable.
    $field = field_info_field('body');
    $field['translatable'] = TRUE;
    field_update_field($field);
  }

  /**
   * Test if field languages are correctly set through the node form.
   */
  function testMultilingualNodeForm() {
    // Create "Basic page" content.
    $langcode = LANGUAGE_NOT_SPECIFIED;
    $title_key = "title";
    $title_value = $this->randomName(8);
    $body_key = "body[$langcode][0][value]";
    $body_value = $this->randomName(16);

    // Create node to edit.
    $edit = array();
    $edit[$title_key] = $title_value;
    $edit[$body_key] = $body_value;
    $edit['langcode'] = 'en';
    $this->drupalPost('node/add/page', $edit, t('Save'));

    // Check that the node exists in the database.
    $node = $this->drupalGetNodeByTitle($edit[$title_key]);
    $this->assertTrue($node, t('Node found in database.'));

    $assert = isset($node->body['en']) && !isset($node->body[LANGUAGE_NOT_SPECIFIED]) && $node->body['en'][0]['value'] == $body_value;
    $this->assertTrue($assert, t('Field language correctly set.'));

    // Change node language.
    $this->drupalGet("node/$node->nid/edit");
    $edit = array(
      $title_key => $this->randomName(8),
      'langcode' => 'it'
    );
    $this->drupalPost(NULL, $edit, t('Save'));
    $node = $this->drupalGetNodeByTitle($edit[$title_key]);
    $this->assertTrue($node, t('Node found in database.'));

    $assert = isset($node->body['it']) && !isset($node->body['en']) && $node->body['it'][0]['value'] == $body_value;
    $this->assertTrue($assert, t('Field language correctly changed.'));

    // Enable content language URL detection.
    language_negotiation_set(LANGUAGE_TYPE_CONTENT, array(LANGUAGE_NEGOTIATION_URL => 0));

    // Test multilingual field language fallback logic.
    $this->drupalGet("it/node/$node->nid");
    $this->assertRaw($body_value, t('Body correctly displayed using Italian as requested language'));

    $this->drupalGet("node/$node->nid");
    $this->assertRaw($body_value, t('Body correctly displayed using English as requested language'));
  }

  /*
   * Test multilingual field display settings.
   */
  function testMultilingualDisplaySettings() {
    // Create "Basic page" content.
    $langcode = LANGUAGE_NOT_SPECIFIED;
    $title_key = "title";
    $title_value = $this->randomName(8);
    $body_key = "body[$langcode][0][value]";
    $body_value = $this->randomName(16);

    // Create node to edit.
    $edit = array();
    $edit[$title_key] = $title_value;
    $edit[$body_key] = $body_value;
    $edit['langcode'] = 'en';
    $this->drupalPost('node/add/page', $edit, t('Save'));

    // Check that the node exists in the database.
    $node = $this->drupalGetNodeByTitle($edit[$title_key]);
    $this->assertTrue($node, t('Node found in database.'));

    // Check if node body is showed.
    $this->drupalGet("node/$node->nid");
    $body = $this->xpath('//article[@id=:id]//div[@class=:class]/descendant::p', array(
      ':id' => 'node-' . $node->nid,
      ':class' => 'content',
    ));
    $this->assertEqual(current($body), $node->body['en'][0]['value'], 'Node body found.');
  }
}

/**
 * Functional tests for comment language.
 */
class LocaleCommentLanguageFunctionalTest extends WebTestBase {
  protected $profile = 'standard';

  public static function getInfo() {
    return array(
      'name' => 'Comment language',
      'description' => 'Tests for comment language.',
      'group' => 'Locale',
    );
  }

  function setUp() {
    // We also use language_test module here to be able to turn on content
    // language negotiation. Drupal core does not provide a way in itself
    // to do that.
    parent::setUp('locale', 'language_test');

    // Create and login user.
    $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'administer content types', 'create article content'));
    $this->drupalLogin($admin_user);

    // Add language.
    $edit = array('predefined_langcode' => 'fr');
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));

    // Set "Article" content type to use multilingual support.
    $edit = array('node_type_language' => 1);
    $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));

    // Enable content language negotiation UI.
    variable_set('language_test_content_language_type', TRUE);

    // Set interface language detection to user and content language detection
    // to URL. Disable inheritance from interface language to ensure content
    // language will fall back to the default language if no URL language can be
    // detected.
    $edit = array(
      'language_interface[enabled][language-user]' => TRUE,
      'language_content[enabled][language-url]' => TRUE,
      'language_content[enabled][language-interface]' => FALSE,
    );
    $this->drupalPost('admin/config/regional/language/detection', $edit, t('Save settings'));

    // Change user language preference, this way interface language is always
    // French no matter what path prefix the URLs have.
    $edit = array('preferred_langcode' => 'fr');
    $this->drupalPost("user/{$admin_user->uid}/edit", $edit, t('Save'));
  }

  /**
   * Test that comment language is properly set.
   */
  function testCommentLanguage() {
    drupal_static_reset('language_list');

    // Create two nodes, one for english and one for french, and comment each
    // node using both english and french as content language by changing URL
    // language prefixes. Meanwhile interface language is always French, which
    // is the user language preference. This way we can ensure that node
    // language and interface language do not influence comment language, as
    // only content language has to.
    foreach (language_list() as $node_langcode => $node_language) {
      $langcode_not_specified = LANGUAGE_NOT_SPECIFIED;

      // Create "Article" content.
      $title = $this->randomName();
      $edit = array(
        "title" => $title,
        "body[$langcode_not_specified][0][value]" => $this->randomName(),
        "langcode" => $node_langcode,
      );
      $this->drupalPost("node/add/article", $edit, t('Save'));
      $node = $this->drupalGetNodeByTitle($title);

      $prefixes = language_negotiation_url_prefixes();
      foreach (language_list() as $langcode => $language) {
        // Post a comment with content language $langcode.
        $prefix = empty($prefixes[$langcode]) ? '' : $prefixes[$langcode] . '/';
        $edit = array("comment_body[$langcode_not_specified][0][value]" => $this->randomName());
        $this->drupalPost("{$prefix}node/{$node->nid}", $edit, t('Save'));

        // Check that comment language matches the current content language.
        $comment = db_select('comment', 'c')
          ->fields('c')
          ->condition('nid', $node->nid)
          ->orderBy('cid', 'DESC')
          ->execute()
          ->fetchObject();
        $args = array('%node_language' => $node_langcode, '%comment_language' => $comment->langcode, '%langcode' => $langcode);
        $this->assertEqual($comment->langcode, $langcode, t('The comment posted with content language %langcode and belonging to the node with language %node_language has language %comment_language', $args));
      }
    }
  }
}

/**
 * Functional tests for localizing date formats.
 */
class LocaleDateFormatsFunctionalTest extends WebTestBase {

  public static function getInfo() {
    return array(
      'name' => 'Localize date formats',
      'description' => 'Tests for the localization of date formats.',
      'group' => 'Locale',
    );
  }

  function setUp() {
    parent::setUp(array('node', 'locale'));

    // Create Article node type.
    $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));

    // Create and login user.
    $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'create article content'));
    $this->drupalLogin($admin_user);
  }

  /**
   * Functional tests for localizing date formats.
   */
  function testLocalizeDateFormats() {
    // Add language.
    $edit = array(
      'predefined_langcode' => 'fr',
    );
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));

    // Set language negotiation.
    $language_type = LANGUAGE_TYPE_INTERFACE;
    $edit = array(
      "{$language_type}[enabled][language-url]" => TRUE,
    );
    $this->drupalPost('admin/config/regional/language/detection', $edit, t('Save settings'));

    // Configure date formats.
    $this->drupalGet('admin/config/regional/date-time/locale');
    $this->assertText('French', 'Configured languages appear.');
    $edit = array(
      'date_format_long' => 'd.m.Y - H:i',
      'date_format_medium' => 'd.m.Y - H:i',
      'date_format_short' => 'd.m.Y - H:i',
    );
    $this->drupalPost('admin/config/regional/date-time/locale/fr/edit', $edit, t('Save configuration'));
    $this->assertText(t('Configuration saved.'), 'French date formats updated.');
    $edit = array(
      'date_format_long' => 'j M Y - g:ia',
      'date_format_medium' => 'j M Y - g:ia',
      'date_format_short' => 'j M Y - g:ia',
    );
    $this->drupalPost('admin/config/regional/date-time/locale/en/edit', $edit, t('Save configuration'));
    $this->assertText(t('Configuration saved.'), 'English date formats updated.');

    // Create node content.
    $node = $this->drupalCreateNode(array('type' => 'article'));

    // Configure format for the node posted date changes with the language.
    $this->drupalGet('node/' . $node->nid);
    $english_date = format_date($node->created, 'custom', 'j M Y');
    $this->assertText($english_date, t('English date format appears'));
    $this->drupalGet('fr/node/' . $node->nid);
    $french_date = format_date($node->created, 'custom', 'd.m.Y');
    $this->assertText($french_date, t('French date format appears'));
  }
}
