locale.test 122 KB
Newer Older
1 2
<?php

3 4
/**
 * @file
5
 * Tests for locale.module.
6
 *
7
 * The test file includes:
8 9 10 11 12 13 14 15 16 17
 *  - 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.
18
 *  - a functional test for multilingual fields.
19
 *  - a functional test for comment language.
20
 *  - a functional test fot language types/negotiation info.
21 22
 */

23 24

/**
25
 * Functional tests for language configuration's effect on negotiation setup.
26 27
 */
class LocaleConfigurationTest extends DrupalWebTestCase {
28
  public static function getInfo() {
29
    return array(
30 31
      'name' => 'Language negotiation autoconfiguration',
      'description' => 'Adds and configures languages to check negotiation changes.',
32
      'group' => 'Locale',
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
    );
  }

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

  /**
   * Functional tests for adding, editing and deleting languages.
   */
  function testLanguageConfiguration() {
    global $base_url;

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

50
    // Check if the Default English language has no path prefix.
51
    $this->drupalGet('admin/config/regional/language/detection/url');
52 53
    $this->assertFieldByXPath('//input[@name="prefix[en]"]', '', t('Default English has no path prefix.'));

54 55
    // Add predefined language.
    $edit = array(
56
      'predefined_langcode' => 'fr',
57
    );
58
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
59
    $this->assertText('French');
60
    $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.'));
61

62
    // Check if the Default English language has no path prefix.
63
    $this->drupalGet('admin/config/regional/language/detection/url');
64 65
    $this->assertFieldByXPath('//input[@name="prefix[en]"]', '', t('Default English has no path prefix.'));
    // Check if French has a path prefix.
66
    $this->drupalGet('admin/config/regional/language/detection/url');
67
    $this->assertFieldByXPath('//input[@name="prefix[fr]"]', 'fr', t('French has a path prefix.'));
68 69

    // Check if we can change the default language.
70
    $this->drupalGet('admin/config/regional/language');
71
    $this->assertFieldChecked('edit-site-default-en', t('English is the default language.'));
72 73
    // Change the default language.
    $edit = array(
74
      'site_default' => 'fr',
75
    );
76
    $this->drupalPost(NULL, $edit, t('Save configuration'));
77 78
    $this->assertNoFieldChecked('edit-site-default-en', t('Default language updated.'));
    $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.'));
79

80
    // Check if a valid language prefix is added afrer changing the default
81
    // language.
82
    $this->drupalGet('admin/config/regional/language/detection/url');
83
    $this->assertFieldByXPath('//input[@name="prefix[en]"]', 'en', t('A valid path prefix has been added to the previous default language.'));
84
    // Check if French still has a path prefix.
85
    $this->drupalGet('admin/config/regional/language/detection/url');
86
    $this->assertFieldByXPath('//input[@name="prefix[fr]"]', 'fr', t('French still has a path prefix.'));
87 88 89
  }
}

90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
/**
 * Functional tests for JavaScript parsing for translatable strings.
 */
class LocaleJavascriptTranslationTest extends DrupalWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'Javascript translation',
      'description' => 'Tests parsing js files for translatable strings',
      'group' => 'Locale',
    );
  }

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

  function testFileParsing() {

    $filename = drupal_get_path('module', 'locale_test') . '/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')
115
      ->fields('s', array('source', 'context'))
116 117 118 119 120 121
      ->condition('s.location', $filename)
      ->execute()
      ->fetchAllKeyed();

    // List of all strings that should be in the file.
    $test_strings = array(
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
      "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",
163 164 165
    );

    // Assert that all strings were found properly.
166 167 168 169 170 171 172 173
    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));
174 175 176 177 178
    }

    $this->assertEqual(count($source_strings), count($test_strings), t("Found correct number of source strings."));
  }
}
179 180 181 182
/**
 * Functional test for string translation and validation.
 */
class LocaleTranslationFunctionalTest extends DrupalWebTestCase {
183
  public static function getInfo() {
184
    return array(
185 186 187
      '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',
188 189 190 191 192 193 194
    );
  }

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

195 196 197 198
  /**
   * Adds a language and tests string translation by users with the appropriate permissions.
   */
  function testStringTranslation() {
199 200 201 202 203 204 205
    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.
206
    $langcode = 'xx';
207 208 209 210 211 212 213
    // 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. Copied straight from locale.inc.
    $language_indicator = "<em class=\"locale-untranslated\">$langcode</em> ";
    // This will be the translation of $name.
    $translation = $this->randomName(16);
214
    $translation_to_en = $this->randomName(16);
215

216
    // Add custom language.
217
    $this->drupalLogin($admin_user);
218
    $edit = array(
219
      'predefined_langcode' => 'custom',
220 221 222 223
      'langcode' => $langcode,
      'name' => $name,
      'direction' => '0',
    );
224
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
225
    // Add string.
226
    t($name, array(), array('langcode' => $langcode));
227
    // Reset locale cache.
228
    locale_reset();
229 230
    $this->assertRaw('"edit-site-default-' . $langcode .'"', t('Language code found.'));
    $this->assertText(t($name), t('Test language added.'));
231
    $this->drupalLogout();
232 233 234

    // Search for the name and translate it.
    $this->drupalLogin($translate_user);
235
    $search = array(
236 237 238 239
      'string' => $name,
      'language' => 'all',
      'translation' => 'all',
    );
240
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
241
    // assertText() seems to remove the input field where $name always could be
242 243
    // found, so this is not a false assert. See how assertNoText succeeds
    // later.
244 245
    $this->assertText($name, t('Search found the name.'));
    $this->assertRaw($language_indicator, t('Name is untranslated.'));
246
    // Assume this is the only result, given the random name.
247 248
    $this->clickLink(t('edit'));
    // We save the lid from the path.
249
    $matches = array();
250
    preg_match('!admin/config/regional/translate/edit/(\d+)!', $this->getUrl(), $matches);
251
    $lid = $matches[1];
252
    // No t() here, it's surely not translated yet.
253
    $this->assertText($name, t('name found on edit screen.'));
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
    $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();
269
    $edit = array(
270 271
      "translations[$langcode][0]" => $translation,
      'translations[en][0]' => $translation_to_en,
272 273
    );
    $this->drupalPost(NULL, $edit, t('Save translations'));
274 275
    $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.'));
276 277 278 279 280 281 282 283 284 285 286
    $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.'));
287
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
288
    // The indicator should not be here.
289
    $this->assertNoRaw($language_indicator, t('String is translated.'));
290 291 292 293

    // 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;
294
    $this->drupalGet('admin/config/regional/translate/edit/' . $random_lid);
295 296
    $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.'));
297
    $this->drupalLogout();
298

299
    // Delete the language.
300
    $this->drupalLogin($admin_user);
301
    $path = 'admin/config/regional/language/delete/' . $langcode;
302 303
    // This a confirm form, we do not need any fields changed.
    $this->drupalPost($path, array(), t('Delete'));
304 305 306
    // 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.'));
307 308
    // Reload to remove $name.
    $this->drupalGet($path);
309 310
    // Verify that language is no longer found.
    $this->assertResponse(404, t('Language no longer found.'));
311
    $this->drupalLogout();
312

313
    // Delete the string.
314
    $this->drupalLogin($translate_user);
315 316 317 318 319
    $search = array(
      'string' => $name,
      'language' => 'all',
      'translation' => 'all',
    );
320
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
321 322
    // Assume this is the only result, given the random name.
    $this->clickLink(t('delete'));
323
    $this->assertText(t('Are you sure you want to delete the string'), t('"delete" link is correct.'));
324
    // Delete the string.
325
    $path = 'admin/config/regional/translate/delete/' . $lid;
326 327 328
    $this->drupalGet($path);
    // First test the 'cancel' link.
    $this->clickLink(t('Cancel'));
329 330
    $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.'));
331
    // Delete the name string.
332
    $this->drupalPost('admin/config/regional/translate/delete/' . $lid, array(), t('Delete'));
333 334
    $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.'));
335
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
336
    $this->assertNoText($name, t('Search now can not find the name.'));
337
  }
338

339 340 341 342 343 344 345 346 347 348 349 350 351 352
  /*
   * 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(
353
      'predefined_langcode' => 'custom',
354 355 356 357 358 359 360 361 362 363 364 365 366 367
      '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');
368
    $result = $query->condition('l.location', '%.js%', 'LIKE')->execute();
369
    $url = 'admin/config/regional/translate/edit/' . $result->fetchObject()->lid;
370
    $edit = array('translations['. $langcode .'][0]' => $this->randomName());
371 372
    $this->drupalPost($url, $edit, t('Save translations'));

373
    // Trigger JavaScript translation parsing and building.
374
    require_once DRUPAL_ROOT . '/core/includes/locale.inc';
375
    _locale_rebuild_js($langcode);
376

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

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

389 390 391 392
  /**
   * Tests the validation of the translation input.
   */
  function testStringValidation() {
393 394
    global $base_url;

395
    // User to add language and strings.
396 397
    $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'translate interface'));
    $this->drupalLogin($admin_user);
398
    $langcode = 'xx';
399 400 401 402 403 404 405 406 407 408 409 410 411
    // 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. Copied straight from locale.inc.
    $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);
412
    $bad_translations[$key] ="<BODY ONLOAD=alert('xss')>" . $key;
413

414 415
    // Add custom language.
    $edit = array(
416
      'predefined_langcode' => 'custom',
417 418 419 420
      'langcode' => $langcode,
      'name' => $name,
      'direction' => '0',
    );
421
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
422
    // Add string.
423
    t($name, array(), array('langcode' => $langcode));
424
    // Reset locale cache.
425
    $search = array(
426 427 428 429
      'string' => $name,
      'language' => 'all',
      'translation' => 'all',
    );
430
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
431
    // Find the edit path.
432
    $content = $this->drupalGetContent();
433
    $this->assertTrue(preg_match('@(admin/config/regional/translate/edit/[0-9]+)@', $content, $matches), t('Found the edit path.'));
434 435
    $path = $matches[0];
    foreach ($bad_translations as $key => $translation) {
436
      $edit = array(
437
        "translations[$langcode][0]" => $translation,
438 439 440 441
      );
      $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');
442 443
      $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.'));
444 445
    }
  }
446 447 448 449 450 451 452 453 454 455 456 457 458

  /**
   * 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.
459
    $langcode = 'xx';
460 461 462 463 464
    // 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. Copied straight from locale.inc.
    $language_indicator = "<em class=\"locale-untranslated\">$langcode</em> ";
465 466
    // This will be the translation of $name.
    $translation = $this->randomName(16);
467 468 469 470

    // Add custom language.
    $this->drupalLogin($admin_user);
    $edit = array(
471
      'predefined_langcode' => 'custom',
472 473 474 475
      'langcode' => $langcode,
      'name' => $name,
      'direction' => '0',
    );
476
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
477
    // Add string.
478
    t($name, array(), array('langcode' => $langcode));
479
    // Reset locale cache.
480
    locale_reset();
481 482 483 484 485 486 487 488 489
    $this->drupalLogout();

    // Search for the name.
    $this->drupalLogin($translate_user);
    $search = array(
      'string' => $name,
      'language' => 'all',
      'translation' => 'all',
    );
490
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
491 492 493
    // 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.
494
    $this->assertText($name, t('Search found the string.'));
495 496 497 498 499 500 501 502

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

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

    // 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();
521
    preg_match('!admin/config/regional/translate/edit/(\d)+!', $this->getUrl(), $matches);
522 523
    $lid = $matches[1];
    $edit = array(
524
      "translations[$langcode][0]" => $translation,
525 526 527 528 529 530 531 532 533 534
    );
    $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',
    );
535
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
536
    $this->assertNoText(t('No strings available.'), t('Search found the translation.'));
537 538 539 540 541 542 543 544

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

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

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

567
    // Ensure translated string doesn't appear if searching in System (English).
568 569
    $search = array(
      'string' => $translation,
570
      'language' => LANGUAGE_SYSTEM,
571 572
      'translation' => 'all',
    );
573
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
574
    $this->assertText(t('No strings available.'), t("Search didn't find the translation."));
575 576 577 578 579 580 581 582

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

588
/**
589
 * Tests plural format handling functionality.
590 591 592 593
 */
class LocalePluralFormatTest extends DrupalWebTestCase {
  public static function getInfo() {
    return array(
594 595
      'name' => 'Plural handling',
      'description' => 'Tests plural handling for various languages.',
596 597 598 599 600
      'group' => 'Locale',
    );
  }

  function setUp() {
601
    parent::setUp('locale');
602 603 604 605 606 607

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

  /**
608
   * Tests locale_get_plural() and format_plural() functionality.
609 610 611 612 613 614 615 616 617 618 619
   */
  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',
    ));

620 621 622 623 624 625 626 627 628
    // 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',
    ));
    $this->importPoFile($this->getPoFileWithBrokenPlural(), array(
      'langcode' => 'hr',
    ));

629 630 631
    // 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');
632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658
    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',
      ),
    );
659

660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685
    // 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,
      ),
    );
686

687 688 689 690 691 692 693 694 695 696 697 698
    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);
      }
    }
  }
699

700 701 702 703 704 705 706 707 708 709 710 711
  /**
   * 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',
    ));
712

713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796
    // 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.'));
797 798
  }

799

800 801 802 803 804 805 806 807 808 809 810 811
  /**
   * 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;
812
    $options['mode'] = LOCALE_IMPORT_OVERWRITE;
813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858
    $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 7\\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 7\\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"

859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893
msgid "Monday"
msgstr "Ponedjeljak"
EOF;
  }

  /**
   * Returns a .po file with a missing plural formula.
   */
  function getPoFileWithMissingPlural() {
    return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 7\\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 7\\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"

894 895 896 897 898 899
msgid "Monday"
msgstr "Ponedjeljak"
EOF;
  }
}

900 901 902 903
/**
 * Functional tests for the import of translation files.
 */
class LocaleImportFunctionalTest extends DrupalWebTestCase {
904
  public static function getInfo() {
905
    return array(
906
      'name' => 'Translation import',
907
      'description' => 'Tests the import of locale files.',
908
      'group' => 'Locale',
909 910 911 912 913 914 915 916 917
    );
  }

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

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

920 921 922
    // Set the translation file directory.
    variable_set('locale_translate_file_directory', drupal_get_path('module', 'locale_test'));

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

  /**
928
   * Test import of standalone .po files.
929 930 931
   */
  function testStandalonePoFile() {
    // Try importing a .po file.
932
    $this->importPoFile($this->getPoFile(), array(
933
      'langcode' => 'fr',
934
    ));
935

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

939 940
    // 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.'));
941

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

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

949

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

955
    // The import should have created 1 string and rejected 2.
956
    $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.'));
957
    $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')));
958
    $this->assertRaw($skip_message, t('Unsafe strings were skipped.'));
959

960

961 962
    // Try importing a .po file which doesn't exist.
    $name = $this->randomName(16);
963
    $this->drupalPost('admin/config/regional/translate/import', array(
964 965 966
      'langcode' => 'fr',
      'files[file]' => $name,
    ), t('Import'));
967 968
    $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.'));
969

970

971 972
    // Try importing a .po file with overriding strings, and ensure existing
    // strings are kept.
973
    $this->importPoFile($this->getOverwritePoFile(), array(
974 975
      'langcode' => 'fr',
      'mode' => 1, // Existing strings are kept, only new strings are added.
976 977
    ));

978
    // The import should have created 1 string.
979
    $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.'));
980 981 982 983 984 985
    // Ensure string wasn't overwritten.
    $search = array(
      'string' => 'Montag',
      'language' => 'fr',
      'translation' => 'translated',
    );
986
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
987
    $this->assertText(t('No strings available.'), t('String not overwritten by imported string.'));
988

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

993 994
    // Try importing a .po file with overriding strings, and ensure existing
    // strings are overwritten.
995
    $this->importPoFile($this->getOverwritePoFile(), array(
996 997
      'langcode' => 'fr',
      'mode' => 0, // Strings in the uploaded file replace existing ones, new ones are added.
998 999
    ));

1000
    // The import should have updated 2 strings.
1001
    $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.'));
1002 1003 1004 1005 1006 1007
    // Ensure string was overwritten.
    $search = array(
      'string' => 'Montag',
      'language' => 'fr',
      'translation' => 'translated',
    );
1008
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
1009
    $this->assertNoText(t('No strings available.'), t('String overwritten by imported string.'));
1010
    // This import should have changed number of plural forms.
1011 1012
    $locale_plurals = variable_get('locale_translation_plurals', array());
    $this->assert($locale_plurals['fr']['plurals'] == 3, t('Plural numbers changed.'));
1013 1014 1015
  }

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

    // Create a custom language.
    $edit = array(
1026
      'predefined_langcode' => 'custom',
1027 1028 1029 1030
      'langcode' => $langcode,
      'name' => $name,
      'direction' => '0',
    );
1031
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
1032 1033 1034

    // Ensure the translation file was automatically imported when language was
    // added.
1035
    $this->assertText(t('One translation file imported.'), t('Language file automatically imported.'));
1036 1037 1038 1039 1040 1041 1042

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

1047
  /**
1048
   * Test msgctxt context support.
1049 1050 1051 1052 1053 1054 1055
   */
  function testLanguageContext() {
    // Try importing a .po file.
    $this->importPoFile($this->getPoFileWithContext(), array(
      'langcode' => 'hr',
    ));

1056 1057
    $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.'));
1058 1059
  }

1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095
  /**
   * 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,
      'mode' => 0,
    ));
    $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. Copied straight from locale.inc.
    $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.'));
  }

1096 1097 1098 1099 1100 1101 1102 1103 1104
  /**
   * 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()) {
1105
    $name = tempnam('temporary://', "po_") . '.po';
1106 1107
    file_put_contents($name, $contents);
    $options['files[file]'] = $name;
1108
    $this->drupalPost('admin/config/regional/translate/import', $options, t('Import'));
1109
    drupal_unlink($name);
1110 1111
  }

1112 1113 1114 1115 1116 1117 1118
  /**
   * Helper function that returns a proper .po file.
   */
  function getPoFile() {
    return <<< EOF
msgid ""
msgstr ""
1119
"Project-Id-Version: Drupal 7\\n"
1120 1121 1122 1123 1124
"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"

1125 1126 1127 1128 1129
msgid "One sheep"
msgid_plural "@count sheep"
msgstr[0] "un mouton"
msgstr[1] "@count moutons"

1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149
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"
1150 1151 1152 1153
EOF;
  }

  /**
1154
   * Helper function that returns a bad .po file.
1155 1156 1157 1158 1159
   */
  function getBadPoFile() {
    return <<< EOF
msgid ""
msgstr ""
1160
"Project-Id-Version: Drupal 7\\n"
1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174
"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>"

1175 1176
EOF;
  }
1177 1178

  /**
1179
   * Helper function that returns a proper .po file for testing.
1180 1181 1182 1183 1184
   */
  function getOverwritePoFile() {
    return <<< EOF
msgid ""
msgstr ""
1185
"Project-Id-Version: Drupal 7\\n"
1186 1187 1188
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
1189
"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"
1190 1191 1192 1193 1194 1195 1196 1197 1198

msgid "Monday"
msgstr "Montag"

msgid "Day"
msgstr "Jour"
EOF;
  }

1199 1200 1201 1202 1203 1204 1205 1206 1207
  /**
   * 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 ""
1208
"Project-Id-Version: Drupal 7\\n"
1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222
"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;
  }

1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261
  /**
   * Helper function that returns a .po file with an empty last item.
   */
  function getPoFileWithEmptyMsgstr() {
    return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 7\\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 7\\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;
  }
1262

1263 1264 1265 1266 1267 1268
}

/**
 * Functional tests for the export of translation files.
 */
class LocaleExportFunctionalTest extends DrupalWebTestCase {
1269
  public static function getInfo() {
1270
    return array(
1271 1272 1273
      'name' => 'Translation export',
      'description' => 'Tests the exportation of locale files.',
      'group' => 'Locale',
1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294
    );
  }

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

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

    $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.
1295
    $name = tempnam('temporary://', "po_") . '.po';