5 * Tests for locale.module.
7 * The test file includes:
8 * - a functional test for the language configuration forms;
9 * - functional tests for the translation functionalities, including searching;
10 * - a functional test for the PO files import feature, including validation;
11 * - functional tests for translations and templates export feature;
12 * - functional tests for the uninstall process;
13 * - a functional test for the language switching feature;
14 * - a functional test for a user's ability to change their default language;
15 * - a functional test for configuring a different path alias per language;
16 * - a functional test for configuring a different path alias per language;
17 * - 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.
25 * Functional tests for the language configuration forms.
27 class LocaleConfigurationTest extends DrupalWebTestCase {
28 public static function getInfo() {
30 'name' => 'Language configuration',
31 'description' => 'Adds a new locale and tests changing its status and the default language.',
37 parent::setUp('locale');
41 * Functional tests for adding, editing and deleting languages.
43 function testLanguageConfiguration() {
46 // User to add and remove language.
47 $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages'));
48 $this->drupalLogin($admin_user);
50 // Add predefined language.
54 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
55 $this->assertText('fr', 'Language added successfully.');
56 $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.');
58 // Add custom language.
59 // Code for the language.
61 // The English name for the language.
62 $name = $this->randomName(16);
63 // The native name for the language.
64 $native = $this->randomName(16);
68 'langcode' => $langcode,
74 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
75 $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.');
76 $this->assertText($langcode, 'Language code found.');
77 $this->assertText($name, 'Name found.');
78 $this->assertText($native, 'Native found.');
79 $this->assertText($native, 'Test language added.');
81 // Check if we can change the default language.
82 $path = 'admin/config/regional/language';
83 $this->drupalGet($path);
84 $this->assertFieldChecked('edit-site-default-en', 'English is the default language.');
85 // Change the default language.
87 'site_default' => $langcode,
89 $this->drupalPost(NULL, $edit, t('Save configuration'));
90 $this->assertNoFieldChecked('edit-site-default-en', 'Default language updated.');
91 $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.');
93 // Check if a valid language prefix is added after changing the default
95 $this->drupalGet('admin/config/regional/language/edit/en');
96 $this->assertFieldByXPath('//input[@name="prefix"]', 'en', 'A valid path prefix has been added to the previous default language.');
98 // Ensure we can't delete the default language.
99 $this->drupalGet('admin/config/regional/language/delete/' . $langcode);
100 $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.');
101 $this->assertText(t('The default language cannot be deleted.'), 'Failed to delete the default language.');
103 // Check if we can disable a language.
105 'enabled[en]' => FALSE,
107 $this->drupalPost($path, $edit, t('Save configuration'));
108 $this->assertNoFieldChecked('edit-enabled-en', 'Language disabled.');
110 // Set disabled language to be the default and ensure it is re-enabled.
112 'site_default' => 'en',
114 $this->drupalPost(NULL, $edit, t('Save configuration'));
115 $this->assertFieldChecked('edit-enabled-en', 'Default language re-enabled.');
117 // Ensure 'edit' link works.
118 $this->clickLink(t('edit'));
119 $this->assertTitle(t('Edit language | Drupal'), 'Page title is "Edit language".');
121 $name = $this->randomName(16);
125 $this->drupalPost('admin/config/regional/language/edit/' . $langcode, $edit, t('Save language'));
126 $this->assertRaw($name, 'The language has been updated.');
127 $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.');
129 // Ensure 'delete' link works.
130 $this->drupalGet('admin/config/regional/language');
131 $this->clickLink(t('delete'));
132 $this->assertText(t('Are you sure you want to delete the language'), '"delete" link is correct.');
133 // Delete an enabled language.
134 $this->drupalGet('admin/config/regional/language/delete/' . $langcode);
135 // First test the 'cancel' link.
136 $this->clickLink(t('Cancel'));
137 $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.');
138 $this->assertRaw($name, 'The language was not deleted.');
139 // Delete the language for real. This a confirm form, we do not need any
141 $this->drupalPost('admin/config/regional/language/delete/' . $langcode, array(), t('Delete'));
142 // We need raw here because %locale will add HTML.
143 $this->assertRaw(t('The language %locale has been removed.', array('%locale' => $name)), 'The test language has been removed.');
144 $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.');
145 // Verify that language is no longer found.
146 $this->drupalGet('admin/config/regional/language/delete/' . $langcode);
147 $this->assertResponse(404, 'Language no longer found.');
148 // Make sure the "language_count" variable has been updated correctly.
149 drupal_static_reset('language_list');
150 $enabled = language_list('enabled');
151 $this->assertEqual(variable_get('language_count', 1), count($enabled[1]), 'Language count is correct.');
152 // Delete a disabled language.
153 // Disable an enabled language.
155 'enabled[fr]' => FALSE,
157 $this->drupalPost($path, $edit, t('Save configuration'));
158 $this->assertNoFieldChecked('edit-enabled-fr', 'French language disabled.');
159 // Get the count of enabled languages.
160 drupal_static_reset('language_list');
161 $enabled = language_list('enabled');
162 // Delete the disabled language.
163 $this->drupalPost('admin/config/regional/language/delete/fr', array(), t('Delete'));
164 // We need raw here because %locale will add HTML.
165 $this->assertRaw(t('The language %locale has been removed.', array('%locale' => 'French')), 'Disabled language has been removed.');
166 $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.');
167 // Verify that language is no longer found.
168 $this->drupalGet('admin/config/regional/language/delete/fr');
169 $this->assertResponse(404, 'Language no longer found.');
170 // Make sure the "language_count" variable has not changed.
171 $this->assertEqual(variable_get('language_count', 1), count($enabled[1]), 'Language count is correct.');
174 // Ensure we can't delete the English language.
175 $this->drupalGet('admin/config/regional/language/delete/en');
176 $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.');
177 $this->assertText(t('The English language cannot be deleted.'), 'Failed to delete English language.');
183 * Tests localization of the JavaScript libraries.
185 * Currently, only the jQuery datepicker is localized using Drupal translations.
187 class LocaleLibraryInfoAlterTest extends DrupalWebTestCase {
188 public static function getInfo() {
190 'name' => 'Javascript library localisation',
191 'description' => 'Tests the localisation of JavaScript libraries.',
197 parent::setUp('locale', 'locale_test');
201 * Verifies that the datepicker can be localized.
203 * @see locale_library_info_alter()
205 public function testLibraryInfoAlter() {
206 drupal_add_library('system', 'ui.datepicker');
207 $scripts = drupal_get_js();
208 $this->assertTrue(strpos($scripts, 'locale.datepicker.js'), 'locale.datepicker.js added to scripts.');
213 * Functional tests for JavaScript parsing for translatable strings.
215 class LocaleJavascriptTranslationTest extends DrupalWebTestCase {
216 public static function getInfo() {
218 'name' => 'Javascript translation',
219 'description' => 'Tests parsing js files for translatable strings',
225 parent::setUp('locale', 'locale_test');
228 function testFileParsing() {
230 $filename = drupal_get_path('module', 'locale_test') . '/locale_test.js';
232 // Parse the file to look for source strings.
233 _locale_parse_js_file($filename);
235 // Get all of the source strings that were found.
236 $source_strings = db_select('locales_source', 's')
237 ->fields('s', array('source', 'context'))
238 ->condition('s.location', $filename)
242 // List of all strings that should be in the file.
243 $test_strings = array(
244 "Standard Call t" => '',
245 "Whitespace Call t" => '',
247 "Single Quote t" => '',
248 "Single Quote \\'Escaped\\' t" => '',
249 "Single Quote Concat strings t" => '',
251 "Double Quote t" => '',
252 "Double Quote \\\"Escaped\\\" t" => '',
253 "Double Quote Concat strings t" => '',
255 "Context !key Args t" => "Context string",
257 "Context Unquoted t" => "Context string unquoted",
258 "Context Single Quoted t" => "Context string single quoted",
259 "Context Double Quoted t" => "Context string double quoted",
261 "Standard Call plural" => '',
262 "Standard Call @count plural" => '',
263 "Whitespace Call plural" => '',
264 "Whitespace Call @count plural" => '',
266 "Single Quote plural" => '',
267 "Single Quote @count plural" => '',
268 "Single Quote \\'Escaped\\' plural" => '',
269 "Single Quote \\'Escaped\\' @count plural" => '',
271 "Double Quote plural" => '',
272 "Double Quote @count plural" => '',
273 "Double Quote \\\"Escaped\\\" plural" => '',
274 "Double Quote \\\"Escaped\\\" @count plural" => '',
276 "Context !key Args plural" => "Context string",
277 "Context !key Args @count plural" => "Context string",
279 "Context Unquoted plural" => "Context string unquoted",
280 "Context Unquoted @count plural" => "Context string unquoted",
281 "Context Single Quoted plural" => "Context string single quoted",
282 "Context Single Quoted @count plural" => "Context string single quoted",
283 "Context Double Quoted plural" => "Context string double quoted",
284 "Context Double Quoted @count plural" => "Context string double quoted",
287 // Assert that all strings were found properly.
288 foreach ($test_strings as $str => $context) {
289 $args = array('%source' => $str, '%context' => $context);
291 // Make sure that the string was found in the file.
292 $this->assertTrue(isset($source_strings[$str]), format_string('Found source string: %source', $args));
294 // Make sure that the proper context was matched.
295 $this->assertTrue(isset($source_strings[$str]) && $source_strings[$str] === $context, strlen($context) > 0 ? format_string('Context for %source is %context', $args) : format_string('Context for %source is blank', $args));
298 $this->assertEqual(count($source_strings), count($test_strings), 'Found correct number of source strings.');
302 * Functional test for string translation and validation.
304 class LocaleTranslationFunctionalTest extends DrupalWebTestCase {
305 public static function getInfo() {
307 'name' => 'String translate, search and validate',
308 'description' => 'Adds a new locale and translates its name. Checks the validation of translation strings and search results.',
314 parent::setUp('locale');
318 * Adds a language and tests string translation by users with the appropriate permissions.
320 function testStringTranslation() {
323 // User to add and remove language.
324 $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages'));
325 // User to translate and delete string.
326 $translate_user = $this->drupalCreateUser(array('translate interface', 'access administration pages'));
327 // Code for the language.
329 // The English name for the language. This will be translated.
330 $name = $this->randomName(16);
331 // The native name for the language.
332 $native = $this->randomName(16);
333 // The domain prefix.
335 // This is the language indicator on the translation search screen for
336 // untranslated strings. Copied straight from locale.inc.
337 $language_indicator = "<em class=\"locale-untranslated\">$langcode</em> ";
338 // This will be the translation of $name.
339 $translation = $this->randomName(16);
341 // Add custom language.
342 $this->drupalLogin($admin_user);
344 'langcode' => $langcode,
350 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
352 t($name, array(), array('langcode' => $langcode));
353 // Reset locale cache.
355 $this->assertText($langcode, 'Language code found.');
356 $this->assertText($name, 'Name found.');
357 $this->assertText($native, 'Native found.');
358 // No t() here, we do not want to add this string to the database and it's
359 // surely not translated yet.
360 $this->assertText($native, 'Test language added.');
361 $this->drupalLogout();
363 // Search for the name and translate it.
364 $this->drupalLogin($translate_user);
368 'translation' => 'all',
371 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
372 // assertText() seems to remove the input field where $name always could be
373 // found, so this is not a false assert. See how assertNoText succeeds
375 $this->assertText($name, 'Search found the name.');
376 $this->assertRaw($language_indicator, 'Name is untranslated.');
377 // Assume this is the only result, given the random name.
378 $this->clickLink(t('edit'));
379 // We save the lid from the path.
381 preg_match('!admin/config/regional/translate/edit/(\d+)!', $this->getUrl(), $matches);
383 // No t() here, it's surely not translated yet.
384 $this->assertText($name, 'name found on edit screen.');
386 "translations[$langcode]" => $translation,
388 $this->drupalPost(NULL, $edit, t('Save translations'));
389 $this->assertText(t('The string has been saved.'), 'The string has been saved.');
390 $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), 'Correct page redirection.');
391 $this->assertTrue($name != $translation && t($name, array(), array('langcode' => $langcode)) == $translation, 't() works.');
392 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
393 // The indicator should not be here.
394 $this->assertNoRaw($language_indicator, 'String is translated.');
396 // Verify that a translation set which has an empty target string can be
397 // updated without any database error.
398 db_update('locales_target')
399 ->fields(array('translation' => ''))
400 ->condition('language', $langcode, '=')
401 ->condition('lid', $lid, '=')
403 $this->drupalPost('admin/config/regional/translate/edit/' . $lid, $edit, t('Save translations'));
404 $this->assertText(t('The string has been saved.'), 'The string has been saved.');
406 // Try to edit a non-existent string and ensure we're redirected correctly.
407 // Assuming we don't have 999,999 strings already.
408 $random_lid = 999999;
409 $this->drupalGet('admin/config/regional/translate/edit/' . $random_lid);
410 $this->assertText(t('String not found'), 'String not found.');
411 $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), 'Correct page redirection.');
412 $this->drupalLogout();
414 // Delete the language.
415 $this->drupalLogin($admin_user);
416 $path = 'admin/config/regional/language/delete/' . $langcode;
417 // This a confirm form, we do not need any fields changed.
418 $this->drupalPost($path, array(), t('Delete'));
419 // We need raw here because %locale will add HTML.
420 $this->assertRaw(t('The language %locale has been removed.', array('%locale' => $name)), 'The test language has been removed.');
421 // Reload to remove $name.
422 $this->drupalGet($path);
423 // Verify that language is no longer found.
424 $this->assertResponse(404, 'Language no longer found.');
425 $this->drupalLogout();
427 // Delete the string.
428 $this->drupalLogin($translate_user);
432 'translation' => 'all',
435 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
436 // Assume this is the only result, given the random name.
437 $this->clickLink(t('delete'));
438 $this->assertText(t('Are you sure you want to delete the string'), '"delete" link is correct.');
439 // Delete the string.
440 $path = 'admin/config/regional/translate/delete/' . $lid;
441 $this->drupalGet($path);
442 // First test the 'cancel' link.
443 $this->clickLink(t('Cancel'));
444 $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), 'Correct page redirection.');
445 $this->assertRaw($name, 'The string was not deleted.');
446 // Delete the name string.
447 $this->drupalPost('admin/config/regional/translate/delete/' . $lid, array(), t('Delete'));
448 $this->assertText(t('The string has been removed.'), 'The string has been removed message.');
449 $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), 'Correct page redirection.');
450 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
451 $this->assertNoText($name, 'Search now can not find the name.');
455 * Adds a language and checks that the JavaScript translation files are
456 * properly created and rebuilt on deletion.
458 function testJavaScriptTranslation() {
459 $user = $this->drupalCreateUser(array('translate interface', 'administer languages', 'access administration pages'));
460 $this->drupalLogin($user);
463 // The English name for the language. This will be translated.
464 $name = $this->randomName(16);
465 // The native name for the language.
466 $native = $this->randomName(16);
467 // The domain prefix.
470 // Add custom language.
472 'langcode' => $langcode,
478 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
479 drupal_static_reset('language_list');
481 // Build the JavaScript translation file.
482 $this->drupalGet('admin/config/regional/translate/translate');
484 // Retrieve the id of the first string available in the {locales_source}
485 // table and translate it.
486 $query = db_select('locales_source', 'l');
487 $query->addExpression('min(l.lid)', 'lid');
488 $result = $query->condition('l.location', '%.js%', 'LIKE')
489 ->condition('l.textgroup', 'default')
491 $url = 'admin/config/regional/translate/edit/' . $result->fetchObject()->lid;
492 $edit = array('translations['. $langcode .']' => $this->randomName());
493 $this->drupalPost($url, $edit, t('Save translations'));
495 // Trigger JavaScript translation parsing and building.
496 require_once DRUPAL_ROOT . '/includes/locale.inc';
497 _locale_rebuild_js($langcode);
499 // Retrieve the JavaScript translation hash code for the custom language to
500 // check that the translation file has been properly built.
501 $file = db_select('languages', 'l')
502 ->fields('l', array('javascript'))
503 ->condition('language', $langcode)
506 $js_file = 'public://' . variable_get('locale_js_directory', 'languages') . '/' . $langcode . '_' . $file->javascript . '.js';
507 $this->assertTrue($result = file_exists($js_file), format_string('JavaScript file created: %file', array('%file' => $result ? $js_file : 'not found')));
509 // Test JavaScript translation rebuilding.
510 file_unmanaged_delete($js_file);
511 $this->assertTrue($result = !file_exists($js_file), format_string('JavaScript file deleted: %file', array('%file' => $result ? $js_file : 'found')));
513 _locale_rebuild_js($langcode);
514 $this->assertTrue($result = file_exists($js_file), format_string('JavaScript file rebuilt: %file', array('%file' => $result ? $js_file : 'not found')));
518 * Tests the validation of the translation input.
520 function testStringValidation() {
523 // User to add language and strings.
524 $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'translate interface'));
525 $this->drupalLogin($admin_user);
527 // The English name for the language. This will be translated.
528 $name = $this->randomName(16);
529 // The native name for the language.
530 $native = $this->randomName(16);
531 // The domain prefix.
533 // This is the language indicator on the translation search screen for
534 // untranslated strings. Copied straight from locale.inc.
535 $language_indicator = "<em class=\"locale-untranslated\">$langcode</em> ";
536 // These will be the invalid translations of $name.
537 $key = $this->randomName(16);
538 $bad_translations[$key] = "<script>alert('xss');</script>" . $key;
539 $key = $this->randomName(16);
540 $bad_translations[$key] = '<img SRC="javascript:alert(\'xss\');">' . $key;
541 $key = $this->randomName(16);
542 $bad_translations[$key] = '<<SCRIPT>alert("xss");//<</SCRIPT>' . $key;
543 $key = $this->randomName(16);
544 $bad_translations[$key] ="<BODY ONLOAD=alert('xss')>" . $key;
546 // Add custom language.
548 'langcode' => $langcode,
554 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
556 t($name, array(), array('langcode' => $langcode));
557 // Reset locale cache.
561 'translation' => 'all',
564 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
565 // Find the edit path.
566 $content = $this->drupalGetContent();
567 $this->assertTrue(preg_match('@(admin/config/regional/translate/edit/[0-9]+)@', $content, $matches), 'Found the edit path.');
569 foreach ($bad_translations as $key => $translation) {
571 "translations[$langcode]" => $translation,
573 $this->drupalPost($path, $edit, t('Save translations'));
574 // Check for a form error on the textarea.
575 $form_class = $this->xpath('//form[@id="locale-translate-edit-form"]//textarea/@class');
576 $this->assertNotIdentical(FALSE, strpos($form_class[0], 'error'), 'The string was rejected as unsafe.');
577 $this->assertNoText(t('The string has been saved.'), 'The string was not saved.');
582 * Tests translation search form.
584 function testStringSearch() {
587 // User to add and remove language.
588 $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages'));
589 // User to translate and delete string.
590 $translate_user = $this->drupalCreateUser(array('translate interface', 'access administration pages'));
592 // Code for the language.
594 // The English name for the language. This will be translated.
595 $name = $this->randomName(16);
596 // The native name for the language.
597 $native = $this->randomName(16);
598 // The domain prefix.
600 // This is the language indicator on the translation search screen for
601 // untranslated strings. Copied straight from locale.inc.
602 $language_indicator = "<em class=\"locale-untranslated\">$langcode</em> ";
603 // This will be the translation of $name.
604 $translation = $this->randomName(16);
606 // Add custom language.
607 $this->drupalLogin($admin_user);
609 'langcode' => $langcode,
615 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
617 t($name, array(), array('langcode' => $langcode));
618 // Reset locale cache.
620 $this->drupalLogout();
622 // Search for the name.
623 $this->drupalLogin($translate_user);
627 'translation' => 'all',
630 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
631 // assertText() seems to remove the input field where $name always could be
632 // found, so this is not a false assert. See how assertNoText succeeds
634 $this->assertText($name, 'Search found the string.');
636 // Ensure untranslated string doesn't appear if searching on 'only
637 // translated strings'.
641 'translation' => 'translated',
644 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
645 $this->assertText(t('No strings available.'), "Search didn't find the string.");
647 // Ensure untranslated string appears if searching on 'only untranslated
648 // strings' in "all" (hasn't been translated to any language).
652 'translation' => 'untranslated',
655 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
656 $this->assertNoText(t('No strings available.'), 'Search found the string.');
658 // Ensure untranslated string appears if searching on 'only untranslated
659 // strings' in the custom language (hasn't been translated to that specific language).
662 'language' => $langcode,
663 'translation' => 'untranslated',
666 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
667 $this->assertNoText(t('No strings available.'), 'Search found the string.');
670 // Assume this is the only result, given the random name.
671 $this->clickLink(t('edit'));
672 // We save the lid from the path.
674 preg_match('!admin/config/regional/translate/edit/(\d)+!', $this->getUrl(), $matches);
677 "translations[$langcode]" => $translation,
679 $this->drupalPost(NULL, $edit, t('Save translations'));
681 // Ensure translated string does appear if searching on 'only
682 // translated strings'.
684 'string' => $translation,
686 'translation' => 'translated',
689 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
690 $this->assertNoText(t('No strings available.'), 'Search found the translation.');
692 // Ensure translated source string doesn't appear if searching on 'only
693 // untranslated strings'.
697 'translation' => 'untranslated',
700 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
701 $this->assertText(t('No strings available.'), "Search didn't find the source string.");
703 // Ensure translated string doesn't appear if searching on 'only
704 // untranslated strings'.
706 'string' => $translation,
708 'translation' => 'untranslated',
711 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
712 $this->assertText(t('No strings available.'), "Search didn't find the translation.");
714 // Ensure translated string does appear if searching on the custom language.
716 'string' => $translation,
717 'language' => $langcode,
718 'translation' => 'all',
721 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
722 $this->assertNoText(t('No strings available.'), 'Search found the translation.');
724 // Ensure translated string doesn't appear if searching on English.
726 'string' => $translation,
728 'translation' => 'all',
731 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
732 $this->assertText(t('No strings available.'), "Search didn't find the translation.");
734 // Search for a string that isn't in the system.
735 $unavailable_string = $this->randomName(16);
737 'string' => $unavailable_string,
739 'translation' => 'all',
742 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
743 $this->assertText(t('No strings available.'), "Search didn't find the invalid string.");
748 * Tests plural index computation functionality.
750 class LocalePluralFormatTest extends DrupalWebTestCase {
751 public static function getInfo() {
753 'name' => 'Plural formula evaluation',
754 'description' => 'Tests plural formula evaluation for various languages.',
760 parent::setUp('locale', 'locale_test');
762 $admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages'));
763 $this->drupalLogin($admin_user);
765 // Import some .po files with formulas to set up the environment.
766 // These will also add the languages to the system and enable them.
767 $this->importPoFile($this->getPoFileWithSimplePlural(), array(
770 $this->importPoFile($this->getPoFileWithComplexPlural(), array(
776 * Tests locale_get_plural() functionality.
778 function testGetPluralFormat() {
779 $this->drupalGet('locale_test_plural_format_page');
780 $tests = _locale_test_plural_format_tests();
782 foreach ($tests as $test) {
783 $this->assertPluralFormat($test['count'], $test['language'], $test['expected-result']);
788 * Helper assert to test locale_get_plural page.
791 * Number for testing.
793 * Language for testing
794 * @param $expected_result
798 function assertPluralFormat($count, $lang, $expected_result) {
799 $message_param = array(
802 '@expected_result' => $expected_result,
804 $message = t("Computed plural index for '@lang' with count @count is @expected_result.", $message_param);
806 $message_param = array(
808 '@expected_result' => $expected_result,
810 $this->assertText(format_string('Language: @lang, locale_get_plural: @expected_result.', $message_param, $message));
814 * Imports a standalone .po file in a given language.
817 * Contents of the .po file to import.
819 * Additional options to pass to the translation import form.
821 function importPoFile($contents, array $options = array()) {
822 $name = tempnam('temporary://', "po_") . '.po';
823 file_put_contents($name, $contents);
824 $options['files[file]'] = $name;
825 $this->drupalPost('admin/config/regional/translate/import', $options, t('Import'));
826 drupal_unlink($name);
830 * Returns a .po file with a simple plural formula.
832 function getPoFileWithSimplePlural() {
836 "Project-Id-Version: Drupal 7\\n"
837 "MIME-Version: 1.0\\n"
838 "Content-Type: text/plain; charset=UTF-8\\n"
839 "Content-Transfer-Encoding: 8bit\\n"
840 "Plural-Forms: nplurals=2; plural=(n!=1);\\n"
843 msgid_plural "@count sheep"
844 msgstr[0] "un mouton"
845 msgstr[1] "@count moutons"
853 * Returns a .po file with a complex plural formula.
855 function getPoFileWithComplexPlural() {
859 "Project-Id-Version: Drupal 7\\n"
860 "MIME-Version: 1.0\\n"
861 "Content-Type: text/plain; charset=UTF-8\\n"
862 "Content-Transfer-Encoding: 8bit\\n"
863 "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"
866 msgid_plural "@count hours"
867 msgstr[0] "@count sat"
868 msgstr[1] "@count sata"
869 msgstr[2] "@count sati"
878 * Functional tests for the import of translation files.
880 class LocaleImportFunctionalTest extends DrupalWebTestCase {
881 public static function getInfo() {
883 'name' => 'Translation import',
884 'description' => 'Tests the import of locale files.',
890 * A user able to create languages and import translations.
892 protected $admin_user = NULL;
895 parent::setUp('locale', 'locale_test');
897 $this->admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages'));
898 $this->drupalLogin($this->admin_user);
902 * Test import of standalone .po files.
904 function testStandalonePoFile() {
905 // Try importing a .po file.
906 $this->importPoFile($this->getPoFile(), array(
910 // The import should automatically create the corresponding language.
911 $this->assertRaw(t('The language %language has been created.', array('%language' => 'French')), 'The language has been automatically created.');
913 // The import should have created 7 strings.
914 $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' => 9, '%update' => 0, '%delete' => 0)), 'The translation file was successfully imported.');
916 // This import should have saved plural forms to have 2 variants.
917 $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, 'Plural number initialized.');
919 // Ensure we were redirected correctly.
920 $this->assertEqual($this->getUrl(), url('admin/config/regional/translate', array('absolute' => TRUE)), 'Correct page redirection.');
923 // Try importing a .po file with invalid tags in the default text group.
924 $this->importPoFile($this->getBadPoFile(), array(
928 // The import should have created 1 string and rejected 2.
929 $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)), 'The translation file was successfully imported.');
930 $skip_message = format_plural(2, 'One translation string was skipped because it contains disallowed HTML.', '@count translation strings were skipped because they contain disallowed HTML.');
931 $this->assertRaw($skip_message, 'Unsafe strings were skipped.');
934 // Try importing a .po file with invalid tags in a non default text group.
935 $this->importPoFile($this->getBadPoFile(), array(
940 // The import should have created 3 strings.
941 $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' => 3, '%update' => 0, '%delete' => 0)), 'The translation file was successfully imported.');
944 // Try importing a .po file which doesn't exist.
945 $name = $this->randomName(16);
946 $this->drupalPost('admin/config/regional/translate/import', array(
948 'files[file]' => $name,
951 $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/import', array('absolute' => TRUE)), 'Correct page redirection.');
952 $this->assertText(t('File to import not found.'), 'File to import not found message.');
955 // Try importing a .po file with overriding strings, and ensure existing
957 $this->importPoFile($this->getOverwritePoFile(), array(
959 'mode' => 1, // Existing strings are kept, only new strings are added.
962 // The import should have created 1 string.
963 $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)), 'The translation file was successfully imported.');
964 // Ensure string wasn't overwritten.
966 'string' => 'Montag',
968 'translation' => 'translated',
971 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
972 $this->assertText(t('No strings available.'), 'String not overwritten by imported string.');
974 // This import should not have changed number of plural forms.
975 $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, 'Plural numbers untouched.');
977 $this->importPoFile($this->getPoFileWithBrokenPlural(), array(
979 'mode' => 1, // Existing strings are kept, only new strings are added.
982 // Attempt to import broken .po file as well to prove that this
983 // will not overwrite the proper plural formula imported above.
984 $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, 'Broken plurals: plural numbers untouched.');
986 $this->importPoFile($this->getPoFileWithMissingPlural(), array(
988 'mode' => 1, // Existing strings are kept, only new strings are added.
991 // Attempt to import .po file which has no plurals and prove that this
992 // will not overwrite the proper plural formula imported above.
993 $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, 'No plurals: plural numbers untouched.');
996 // Try importing a .po file with overriding strings, and ensure existing
997 // strings are overwritten.
998 $this->importPoFile($this->getOverwritePoFile(), array(
1000 'mode' => 0, // Strings in the uploaded file replace existing ones, new ones are added.
1003 // The import should have updated 2 strings.
1004 $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)), 'The translation file was successfully imported.');
1005 // Ensure string was overwritten.
1007 'string' => 'Montag',
1009 'translation' => 'translated',
1012 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
1013 $this->assertNoText(t('No strings available.'), 'String overwritten by imported string.');
1014 // This import should have changed number of plural forms.
1015 $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 3, 'Plural numbers changed.');
1019 * Test automatic import of a module's translation files when a language is
1022 function testAutomaticModuleTranslationImportLanguageEnable() {
1023 // Code for the language - manually set to match the test translation file.
1025 // The English name for the language.
1026 $name = $this->randomName(16);
1027 // The native name for the language.
1028 $native = $this->randomName(16);
1029 // The domain prefix.
1030 $prefix = $langcode;
1032 // Create a custom language.
1034 'langcode' => $langcode,
1036 'native' => $native,
1037 'prefix' => $prefix,
1040 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
1042 // Ensure the translation file was automatically imported when language was
1044 $this->assertText(t('One translation file imported for the enabled modules.'), 'Language file automatically imported.');
1046 // Ensure strings were successfully imported.
1048 'string' => 'lundi',
1049 'language' => $langcode,
1050 'translation' => 'translated',
1053 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
1054 $this->assertNoText(t('No strings available.'), 'String successfully imported.');
1058 * Test msgctxt context support.
1060 function testLanguageContext() {
1061 // Try importing a .po file.
1062 $this->importPoFile($this->getPoFileWithContext(), array(
1066 $this->assertIdentical(t('May', array(), array('langcode' => 'hr', 'context' => 'Long month name')), 'Svibanj', 'Long month name context is working.');
1067 $this->assertIdentical(t('May', array(), array('langcode' => 'hr')), 'Svi.', 'Default context is working.');
1071 * Test empty msgstr at end of .po file see #611786.
1073 function testEmptyMsgstr() {
1076 // Try importing a .po file.
1077 $this->importPoFile($this->getPoFileWithMsgstr(), array(
1078 'langcode' => $langcode,
1081 $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)), 'The translation file was successfully imported.');
1082 $this->assertIdentical(t('Operations', array(), array('langcode' => $langcode)), 'Műveletek', 'String imported and translated.');
1084 // Try importing a .po file.
1085 $this->importPoFile($this->getPoFileWithEmptyMsgstr(), array(
1086 'langcode' => $langcode,
1089 $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)), 'The translation file was successfully imported.');
1090 // This is the language indicator on the translation search screen for
1091 // untranslated strings. Copied straight from locale.inc.
1092 $language_indicator = "<em class=\"locale-untranslated\">$langcode</em> ";
1093 $str = "Operations";
1096 'language' => 'all',
1097 'translation' => 'all',
1100 $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
1101 // assertText() seems to remove the input field where $str always could be
1102 // found, so this is not a false assert.
1103 $this->assertText($str, 'Search found the string.');
1104 $this->assertRaw($language_indicator, 'String is untranslated again.');
1108 * Helper function: import a standalone .po file in a given language.
1111 * Contents of the .po file to import.
1113 * Additional options to pass to the translation import form.
1115 function importPoFile($contents, array $options = array()) {
1116 $name = tempnam('temporary://', "po_") . '.po';
1117 file_put_contents($name, $contents);
1118 $options['files[file]'] = $name;
1119 $this->drupalPost('admin/config/regional/translate/import', $options, t('Import'));
1120 drupal_unlink($name);
1124 * Helper function that returns a proper .po file.
1126 function getPoFile() {
1130 "Project-Id-Version: Drupal 7\\n"
1131 "MIME-Version: 1.0\\n"
1132 "Content-Type: text/plain; charset=UTF-8\\n"
1133 "Content-Transfer-Encoding: 8bit\\n"
1134 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
1137 msgid_plural "@count sheep"
1138 msgstr[0] "un mouton"
1139 msgstr[1] "@count moutons"
1165 * Helper function that returns a bad .po file.
1167 function getBadPoFile() {
1171 "Project-Id-Version: Drupal 7\\n"
1172 "MIME-Version: 1.0\\n"
1173 "Content-Type: text/plain; charset=UTF-8\\n"
1174 "Content-Transfer-Encoding: 8bit\\n"
1175 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
1177 msgid "Save configuration"
1178 msgstr "Enregistrer la configuration"
1181 msgstr "modifier<img SRC="javascript:alert(\'xss\');">"
1184 msgstr "supprimer<script>alert('xss');</script>"
1190 * Helper function that returns a proper .po file, for testing overwriting
1191 * existing translations.
1193 function getOverwritePoFile() {
1197 "Project-Id-Version: Drupal 7\\n"
1198 "MIME-Version: 1.0\\n"
1199 "Content-Type: text/plain; charset=UTF-8\\n"
1200 "Content-Transfer-Encoding: 8bit\\n"
1201 "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"
1212 * Helper function that returns a .po file with context.
1214 function getPoFileWithContext() {
1215 // Croatian (code hr) is one of the languages that have a different
1216 // form for the full name and the abbreviated name for the month May.
1220 "Project-Id-Version: Drupal 7\\n"
1221 "MIME-Version: 1.0\\n"
1222 "Content-Type: text/plain; charset=UTF-8\\n"
1223 "Content-Transfer-Encoding: 8bit\\n"
1224 "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"
1226 msgctxt "Long month name"
1236 * Helper function that returns a .po file with an empty last item.
1238 function getPoFileWithEmptyMsgstr() {
1242 "Project-Id-Version: Drupal 7\\n"
1243 "MIME-Version: 1.0\\n"
1244 "Content-Type: text/plain; charset=UTF-8\\n"
1245 "Content-Transfer-Encoding: 8bit\\n"
1246 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
1254 * Helper function that returns a .po file with an empty last item.
1256 function getPoFileWithMsgstr() {
1260 "Project-Id-Version: Drupal 7\\n"
1261 "MIME-Version: 1.0\\n"
1262 "Content-Type: text/plain; charset=UTF-8\\n"
1263 "Content-Transfer-Encoding: 8bit\\n"
1264 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
1269 msgid "Will not appear in Drupal core, so we can ensure the test passes"
1277 * Returns a .po file with a missing plural formula.
1279 function getPoFileWithMissingPlural() {
1283 "Project-Id-Version: Drupal 7\\n"
1284 "MIME-Version: 1.0\\n"
1285 "Content-Type: text/plain; charset=UTF-8\\n"
1286 "Content-Transfer-Encoding: 8bit\\n"
1289 msgstr "Ponedjeljak"
1294 * Returns a .po file with a broken plural formula.
1296 function getPoFileWithBrokenPlural() {
1300 "Project-Id-Version: Drupal 7\\n"
1301 "MIME-Version: 1.0\\n"
1302 "Content-Type: text/plain; charset=UTF-8\\n"
1303 "Content-Transfer-Encoding: 8bit\\n"
1304 "Plural-Forms: broken, will not parse\\n"
1314 * Functional tests for the export of translation files.
1316 class LocaleExportFunctionalTest extends DrupalWebTestCase {
1317 public static function getInfo() {
1319 'name' => 'Translation export',
1320 'description' => 'Tests the exportation of locale files.',
1321 'group' => 'Locale',
1326 * A user able to create languages and export translations.
1328 protected $admin_user = NULL;
1331 parent::setUp('locale', 'locale_test');
1333 $this->admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages'));
1334 $this->drupalLogin($this->admin_user);
1338 * Test exportation of translations.
1340 function testExportTranslation() {
1341 // First import some known translations.
1342 // This will also automatically enable the 'fr' language.
1343 $name = tempnam('temporary://', "po_") . '.po';
1344 file_put_contents($name, $this->getPoFile());
1345 $this->drupalPost('admin/config/regional/translate/import', array(
1347 'files[file]' => $name,
1349 drupal_unlink($name);
1351 // Get the French translations.
1352 $this->drupalPost('admin/config/regional/translate/export', array(
1356 // Ensure we have a translation file.
1357 $this->assertRaw('# French translation of Drupal', 'Exported French translation file.');
1358 // Ensure our imported translations exist in the file.
1359 $this->assertRaw('msgstr "lundi"', 'French translations present in exported file.');
1363 * Test exportation of translation template file.
1365 function testExportTranslationTemplateFile() {
1366 // Get the translation template file.
1367 // There are two 'Export' buttons on this page, but it somehow works. It'd
1368 // be better if we could use the submit button id like documented but that
1370 $this->drupalPost('admin/config/regional/translate/export', array(), t('Export'));
1371 // Ensure we have a translation file.
1372 $this->assertRaw('# LANGUAGE translation of PROJECT', 'Exported translation template file.');
1376 * Helper function that returns a proper .po file.
1378 function getPoFile() {
1382 "Project-Id-Version: Drupal 6\\n"
1383 "MIME-Version: 1.0\\n"
1384 "Content-Type: text/plain; charset=UTF-8\\n"
1385 "Content-Transfer-Encoding: 8bit\\n"
1386 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
1396 * Tests for the st() function.
1398 class LocaleInstallTest extends DrupalWebTestCase {
1399 public static function getInfo() {
1401 'name' => 'String translation using st()',
1402 'description' => 'Tests that st() works like t().',
1403 'group' => 'Locale',
1408 parent::setUp('locale');
1410 // st() lives in install.inc, so ensure that it is loaded for all tests.
1411 require_once DRUPAL_ROOT . '/includes/install.inc';
1415 * Verify that function signatures of t() and st() are equal.
1417 function testFunctionSignatures() {
1418 $reflector_t = new ReflectionFunction('t');
1419 $reflector_st = new ReflectionFunction('st');
1420 $this->assertEqual($reflector_t->getParameters(), $reflector_st->getParameters(), 'Function signatures of t() and st() are equal.');
1425 * Locale uninstall with English UI functional test.
1427 class LocaleUninstallFunctionalTest extends DrupalWebTestCase {
1428 public static function getInfo() {
1430 'name' => 'Locale uninstall (EN)',
1431 'description' => 'Tests the uninstall process using the built-in UI language.',
1432 'group' => 'Locale',
1437 * The default language set for the UI before uninstall.
1439 protected $language;
1442 parent::setUp('locale');
1443 $this->language = 'en';
1447 * Check if the values of the Locale variables are correct after uninstall.
1449 function testUninstallProcess() {
1450 $locale_module = array('locale');
1452 // Add a new language and optionally set it as default.
1453 require_once DRUPAL_ROOT . '/includes/locale.inc';
1454 locale_add_language('fr', 'French', 'Français', LANGUAGE_LTR, '', '', TRUE, $this->language == 'fr');
1456 // Check the UI language.
1457 drupal_language_initialize();
1459 $this->assertEqual($language->language, $this->language, format_string('Current language: %lang', array('%lang' => $language->language)));
1461 // Enable multilingual workflow option for articles.
1462 variable_set('language_content_type_article', 1);
1464 // Change JavaScript translations directory.
1465 variable_set('locale_js_directory', 'js_translations');
1467 // Build the JavaScript translation file for French.
1468 $user = $this->drupalCreateUser(array('translate interface', 'access administration pages'));
1469 $this->drupalLogin($user);
1470 $this->drupalGet('admin/config/regional/translate/translate');
1471 $string = db_query('SELECT min(lid) AS lid FROM {locales_source} WHERE location LIKE :location AND textgroup = :textgroup', array(
1472 ':location' => '%.js%',
1473 ':textgroup' => 'default',
1475 $edit = array('translations[fr]' => 'french translation');
1476 $this->drupalPost('admin/config/regional/translate/edit/' . $string->lid, $edit, t('Save translations'));
1477 _locale_rebuild_js('fr');
1478 $file = db_query('SELECT javascript FROM {languages} WHERE language = :language', array(':language' => 'fr'))->fetchObject();
1479 $js_file = 'public://' . variable_get('locale_js_directory', 'languages') . '/fr_' . $file->javascript . '.js';
1480 $this->assertTrue($result = file_exists($js_file), format_string('JavaScript file created: %file', array('%file' => $result ? $js_file : 'none')));
1482 // Disable string caching.
1483 variable_set('locale_cache_strings', 0);
1485 // Change language negotiation options.
1486 drupal_load('module', 'locale');
1487 variable_set('language_types', drupal_language_types() + array('language_custom' => TRUE));
1488 variable_set('language_negotiation_' . LANGUAGE_TYPE_INTERFACE, locale_language_negotiation_info());
1489 variable_set('language_negotiation_' . LANGUAGE_TYPE_CONTENT, locale_language_negotiation_info());
1490 variable_set('language_negotiation_' . LANGUAGE_TYPE_URL, locale_language_negotiation_info());
1492 // Change language providers settings.
1493 variable_set('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX);
1494 variable_set('locale_language_negotiation_session_param', TRUE);
1496 // Uninstall Locale.
1497 module_disable($locale_module);
1498 drupal_uninstall_modules($locale_module);
1500 // Visit the front page.
1501 $this->drupalGet('');
1503 // Check the init language logic.
1504 drupal_language_initialize();
1505 $this->assertEqual($language->language, 'en', format_string('Language after uninstall: %lang', array('%lang' => $language->language)));
1507 // Check JavaScript files deletion.
1508 $this->assertTrue($result = !file_exists($js_file), format_string('JavaScript file deleted: %file', array('%file' => $result ? $js_file : 'found')));
1510 // Check language count.
1511 $language_count = variable_get('language_count', 1);
1512 $this->assertEqual($language_count, 1, format_string('Language count: %count', array('%count' => $language_count)));
1514 // Check language negotiation.
1515 require_once DRUPAL_ROOT . '/includes/language.inc';
1516 $this->assertTrue(count(language_types()) == count(drupal_language_types()), 'Language types reset');
1517 $language_negotiation = language_negotiation_get(LANGUAGE_TYPE_INTERFACE) == LANGUAGE_NEGOTIATION_DEFAULT;
1518 $this->assertTrue($language_negotiation, format_string('Interface language negotiation: %setting', array('%setting' => $language_negotiation ? 'none' : 'set')));
1519 $language_negotiation = language_negotiation_get(LANGUAGE_TYPE_CONTENT) == LANGUAGE_NEGOTIATION_DEFAULT;
1520 $this->assertTrue($language_negotiation, format_string('Content language negotiation: %setting', array('%setting' => $language_negotiation ? 'none' : 'set')));
1521 $language_negotiation = language_negotiation_get(LANGUAGE_TYPE_URL) == LANGUAGE_NEGOTIATION_DEFAULT;
1522 $this->assertTrue($language_negotiation, format_string('URL language negotiation: %setting', array('%setting' => $language_negotiation ? 'none' : 'set')));
1524 // Check language providers settings.
1525 $this->assertFalse(variable_get('locale_language_negotiation_url_part', FALSE), 'URL language provider indicator settings cleared.');
1526 $this->assertFalse(variable_get('locale_language_negotiation_session_param', FALSE), 'Visit language provider settings cleared.');
1528 // Check JavaScript parsed.
1529 $javascript_parsed_count = count(variable_get('javascript_parsed', array()));
1530 $this->assertEqual($javascript_parsed_count, 0, format_string('JavaScript parsed count: %count', array('%count' => $javascript_parsed_count)));
1532 // Check multilingual workflow option for articles.
1533 $multilingual = variable_get('language_content_type_article', 0);
1534 $this->assertEqual($multilingual, 0, format_string('Multilingual workflow option: %status', array('%status' => $multilingual ? 'enabled': 'disabled')));
1536 // Check JavaScript translations directory.
1537 $locale_js_directory = variable_get('locale_js_directory', 'languages');
1538 $this->assertEqual($locale_js_directory, 'languages', format_string('JavaScript translations directory: %dir', array('%dir' => $locale_js_directory)));
1540 // Check string caching.
1541 $locale_cache_strings = variable_get('locale_cache_strings', 1);
1542 $this->assertEqual($locale_cache_strings, 1, format_string('String caching: %status', array('%status' => $locale_cache_strings ? 'enabled': 'disabled')));
1547 * Locale uninstall with French UI functional test.
1549 * Because this class extends LocaleUninstallFunctionalTest, it doesn't require a new
1550 * test of its own. Rather, it switches the default UI language in setUp and then
1551 * runs the testUninstallProcess (which it inherits from LocaleUninstallFunctionalTest)
1552 * to test with this new language.
1554 class LocaleUninstallFrenchFunctionalTest extends LocaleUninstallFunctionalTest {
1555 public static function getInfo() {
1557 'name' => 'Locale uninstall (FR)',
1558 'description' => 'Tests the uninstall process using French as interface language.',
1559 'group' => 'Locale',
1565 $this->language = 'fr';
1570 * Functional tests for the language switching feature.
1572 class LocaleLanguageSwitchingFunctionalTest extends DrupalWebTestCase {
1574 public static function getInfo() {
1576 'name' => 'Language switching',
1577 'description' => 'Tests for the language switching feature.',
1578 'group' => 'Locale',
1583 parent::setUp('locale');
1585 // Create and login user.
1586 $admin_user = $this->drupalCreateUser(array('administer blocks', 'administer languages', 'translate interface', 'access administration pages'));
1587 $this->drupalLogin($admin_user);
1591 * Functional tests for the language switcher block.
1593 function testLanguageBlock() {
1594 // Enable the language switching block.
1595 $language_type = LANGUAGE_TYPE_INTERFACE;
1597 "blocks[locale_{$language_type}][region]" => 'sidebar_first',
1599 $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
1605 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
1607 // Enable URL language detection and selection.
1608 $edit = array('language[enabled][locale-url]' => '1');
1609 $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings'));
1611 // Assert that the language switching block is displayed on the frontpage.
1612 $this->drupalGet('');
1613 $this->assertText(t('Languages'), 'Language switcher block found.');
1615 // Assert that only the current language is marked as active.
1616 list($language_switcher) = $this->xpath('//div[@id=:id]/div[@class="content"]', array(':id' => 'block-locale-' . $language_type));
1618 'active' => array(),
1619 'inactive' => array(),
1622 'active' => array(),
1623 'inactive' => array(),
1625 foreach ($language_switcher->ul->li as $link) {
1626 $classes = explode(" ", (string) $link['class']);
1627 list($language) = array_intersect($classes, array('en', 'fr'));
1628 if (in_array('active', $classes)) {
1629 $links['active'][] = $language;
1632 $links['inactive'][] = $language;
1634 $anchor_classes = explode(" ", (string) $link->a['class']);
1635 if (in_array('active', $anchor_classes)) {
1636 $anchors['active'][] = $language;
1639 $anchors['inactive'][] = $language;
1642 $this->assertIdentical($links, array('active' => array('en'), 'inactive' => array('fr')), 'Only the current language list item is marked as active on the language switcher block.');
1643 $this->assertIdentical($anchors, array('active' => array('en'), 'inactive' => array('fr')), 'Only the current language anchor is marked as active on the language switcher block.');
1648 * Test browser language detection.
1650 class LocaleBrowserDetectionTest extends DrupalUnitTestCase {
1652 public static function getInfo() {
1654 'name' => 'Browser language detection',
1655 'description' => 'Tests for the browser language detection.',
1656 'group' => 'Locale',
1661 * Unit tests for the locale_language_from_browser() function.
1663 function testLanguageFromBrowser() {
1664 // Load the required functions.
1665 require_once DRUPAL_ROOT . '/includes/locale.inc';
1668 // In our test case, 'en' has priority over 'en-US'.
1669 'en' => (object) array(
1672 'en-US' => (object) array(
1673 'language' => 'en-US',
1675 // But 'fr-CA' has priority over 'fr'.
1676 'fr-CA' => (object) array(
1677 'language' => 'fr-CA',
1679 'fr' => (object) array(
1682 // 'es-MX' is alone.
1683 'es-MX' => (object) array(
1684 'language' => 'es-MX',
1687 'pt' => (object) array(
1690 // Language codes with more then one dash are actually valid.
1691 // eh-oh-laa-laa is the official language code of the Teletubbies.
1692 'eh-oh-laa-laa' => (object) array(
1693 'language' => 'eh-oh-laa-laa',
1697 $test_cases = array(
1698 // Equal qvalue for each language, choose the site preferred one.
1699 'en,en-US,fr-CA,fr,es-MX' => 'en',
1700 'en-US,en,fr-CA,fr,es-MX' => 'en',
1705 'fr,fr-CA' => 'fr-CA',
1706 'fr-CA,fr' => 'fr-CA',
1708 'fr;q=1' => 'fr-CA',
1709 'fr,es-MX' => 'fr-CA',
1712 'es-MX,de' => 'es-MX',
1713 'de,es-MX' => 'es-MX',
1715 // Different cases and whitespace.
1723 // A less specific language from the browser matches a more specific one
1724 // from the website, and the other way around for compatibility with
1725 // some versions of Internet Explorer.
1730 'pt-PT;q=0.5,pt-BR;q=1,en;q=0.7' => 'en',
1731 'pt-PT;q=1,pt-BR;q=0.5,en;q=0.7' => 'en',
1732 'pt-PT;q=0.4,pt-BR;q=0.1,en;q=0.7' => 'en',
1733 'pt-PT;q=0.1,pt-BR;q=0.4,en;q=0.7' => 'en',
1735 // Language code with several dashes are valid. The less specific language
1736 // from the browser matches the more specific one from the website.
1737 'eh-oh-laa-laa' => 'eh-oh-laa-laa',
1738 'eh-oh-laa' => 'eh-oh-laa-laa',
1739 'eh-oh' => 'eh-oh-laa-laa',
1740 'eh' => 'eh-oh-laa-laa',
1742 // Different qvalues.
1743 'fr,en;q=0.5' => 'fr-CA',
1744 'fr,en;q=0.5,fr-CA;q=0.25' => 'fr',
1746 // Silly wildcards are also valid.
1747 '*,fr-CA;q=0.5' => 'en',
1748 '*,en;q=0.25' => 'fr-CA',
1749 'en,en-US;q=0.5,fr;q=0.25' => 'en',
1750 'en-US,en;q=0.5,fr;q=0.25' => 'en-US',
1752 // Unresolvable cases.
1755 'iecRswK4eh' => FALSE,
1756 $this->randomName(10) => FALSE,
1759 foreach ($test_cases as $accept_language => $expected_result) {
1760 $_SERVER['HTTP_ACCEPT_LANGUAGE'] = $accept_language;
1761 $result = locale_language_from_browser($languages);
1762 $this->assertIdentical($result, $expected_result, format_string("Language selection '@accept-language' selects '@result', result = '@actual'", array('@accept-language' => $accept_language, '@result' => $expected_result, '@actual' => isset($result) ? $result : 'none')));
1768 * Functional tests for a user's ability to change their default language.
1770 class LocaleUserLanguageFunctionalTest extends DrupalWebTestCase {
1771 public static function getInfo() {
1773 'name' => 'User language settings',
1774 'description' => "Tests user's ability to change their default language.",
1775 'group' => 'Locale',
1780 parent::setUp('locale');
1784 * Test if user can change their default language.
1786 function testUserLanguageConfiguration() {
1789 // User to add and remove language.
1790 $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages'));
1791 // User to change their default language.
1792 $web_user = $this->drupalCreateUser();
1794 // Add custom language.
1795 $this->drupalLogin($admin_user);
1796 // Code for the language.
1798 // The English name for the language.
1799 $name = $this->randomName(16);
1800 // The native name for the language.
1801 $native = $this->randomName(16);
1802 // The domain prefix.
1805 'langcode' => $langcode,
1807 'native' => $native,
1808 'prefix' => $prefix,
1811 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
1813 // Add custom language and disable it.
1814 // Code for the language.
1815 $langcode_disabled = 'xx-yy';
1816 // The English name for the language. This will be translated.
1817 $name_disabled = $this->randomName(16);
1818 // The native name for the language.
1819 $native_disabled = $this->randomName(16);
1820 // The domain prefix.
1821 $prefix_disabled = $langcode_disabled;
1823 'langcode' => $langcode_disabled,
1824 'name' => $name_disabled,
1825 'native' => $native_disabled,
1826 'prefix' => $prefix_disabled,
1829 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
1830 // Disable the language.
1832 'enabled[' . $langcode_disabled . ']' => FALSE,
1834 $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration'));
1835 $this->drupalLogout();
1837 // Login as normal user and edit account settings.
1838 $this->drupalLogin($web_user);
1839 $path = 'user/' . $web_user->uid . '/edit';
1840 $this->drupalGet($path);
1841 // Ensure language settings fieldset is available.
1842 $this->assertText(t('Language settings'), 'Language settings available.');
1843 // Ensure custom language is present.
1844 $this->assertText($name, 'Language present on form.');
1845 // Ensure disabled language isn't present.
1846 $this->assertNoText($name_disabled, 'Disabled language not present on form.');
1847 // Switch to our custom language.
1849 'language' => $langcode,
1851 $this->drupalPost($path, $edit, t('Save'));
1852 // Ensure form was submitted successfully.
1853 $this->assertText(t('The changes have been saved.'), 'Changes were saved.');
1854 // Check if language was changed.
1855 $elements = $this->xpath('//input[@id=:id]', array(':id' => 'edit-language-' . $langcode));
1856 $this->assertTrue(isset($elements[0]) && !empty($elements[0]['checked']), 'Default language successfully updated.');
1858 $this->drupalLogout();
1863 * Functional test for language handling during user creation.
1865 class LocaleUserCreationTest extends DrupalWebTestCase {
1867 public static function getInfo() {
1869 'name' => 'User creation',
1870 'description' => 'Tests whether proper language is stored for new users and access to language selector.',
1871 'group' => 'Locale',
1876 parent::setUp('locale');
1877 variable_set('user_register', USER_REGISTER_VISITORS);
1881 * Functional test for language handling during user creation.
1883 function testLocalUserCreation() {
1884 // User to add and remove language and create new users.
1885 $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'administer users'));
1886 $this->drupalLogin($admin_user);
1888 // Add predefined language.
1893 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
1894 $this->assertText($langcode, 'Language added successfully.');
1895 $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), 'Correct page redirection.');
1897 // Set language negotiation.
1899 'language[enabled][locale-url]' => TRUE,
1901 $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings'));
1902 $this->assertText(t('Language negotiation configuration saved.'), 'Set language negotiation.');
1904 // Check if the language selector is available on admin/people/create and
1905 // set to the currently active language.
1906 $this->drupalGet($langcode . '/admin/people/create');
1907 $this->assertFieldChecked("edit-language-$langcode", 'Global language set in the language selector.');
1909 // Create a user with the admin/people/create form and check if the correct
1911 $username = $this->randomName(10);
1913 'name' => $username,
1914 'mail' => $this->randomName(4) . '@example.com',
1915 'pass[pass1]' => $username,
1916 'pass[pass2]' => $username,
1919 $this->drupalPost($langcode . '/admin/people/create', $edit, t('Create new account'));
1921 $user = user_load_by_name($username);
1922 $this->assertEqual($user->language, $langcode, 'New user has correct language set.');
1924 // Register a new user and check if the language selector is hidden.
1925 $this->drupalLogout();
1927 $this->drupalGet($langcode . '/user/register');
1928 $this->assertNoFieldByName('language[fr]', 'Language selector is not accessible.');
1930 $username = $this->randomName(10);
1932 'name' => $username,
1933 'mail' => $this->randomName(4) . '@example.com',
1936 $this->drupalPost($langcode . '/user/register', $edit, t('Create new account'));
1938 $user = user_load_by_name($username);
1939 $this->assertEqual($user->language, $langcode, 'New user has correct language set.');
1941 // Test if the admin can use the language selector and if the
1942 // correct language is was saved.
1943 $user_edit = $langcode . '/user/' . $user->uid . '/edit';
1945 $this->drupalLogin($admin_user);
1946 $this->drupalGet($user_edit);
1947 $this->assertFieldChecked("edit-language-$langcode", 'Language selector is accessible and correct language is selected.');
1949 // Set pass_raw so we can login the new user.
1950 $user->pass_raw = $this->randomName(10);
1952 'pass[pass1]' => $user->pass_raw,
1953 'pass[pass2]' => $user->pass_raw,
1956 $this->drupalPost($user_edit, $edit, t('Save'));
1958 $this->drupalLogin($user);
1959 $this->drupalGet($user_edit);
1960 $this->assertFieldChecked("edit-language-$langcode", 'Language selector is accessible and correct language is selected.');
1965 * Functional tests for configuring a different path alias per language.
1967 class LocalePathFunctionalTest extends DrupalWebTestCase {
1968 public static function getInfo() {
1970 'name' => 'Path language settings',
1971 'description' => 'Checks you can configure a language for individual URL aliases.',
1972 'group' => 'Locale',
1977 parent::setUp('locale', 'path');
1981 * Test if a language can be associated with a path alias.
1983 function testPathLanguageConfiguration() {
1986 // User to add and remove language.
1987 $admin_user = $this->drupalCreateUser(array('administer languages', 'create page content', 'administer url aliases', 'create url aliases', 'access administration pages'));
1989 // Add custom language.
1990 $this->drupalLogin($admin_user);
1991 // Code for the language.
1993 // The English name for the language.
1994 $name = $this->randomName(16);
1995 // The native name for the language.
1996 $native = $this->randomName(16);
1997 // The domain prefix.
1998 $prefix = $langcode;
2000 'langcode' => $langcode,
2002 'native' => $native,
2003 'prefix' => $prefix,
2006 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
2008 // Check that the "xx" front page is not available when path prefixes are
2010 $this->drupalPost('admin/config/regional/language/configure', array(), t('Save settings'));
2011 $this->drupalGet($prefix);
2012 $this->assertResponse(404, 'The "xx" front page is not available yet.');
2014 // Enable URL language detection and selection.
2015 $edit = array('language[enabled][locale-url]' => 1);
2016 $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings'));
2019 $node = $this->drupalCreateNode(array('type' => 'page'));
2021 // Create a path alias in default language (English).
2022 $path = 'admin/config/search/path/add';
2023 $english_path = $this->randomName(8);
2025 'source' => 'node/' . $node->nid,
2026 'alias' => $english_path,
2029 $this->drupalPost($path, $edit, t('Save'));
2031 // Create a path alias in new custom language.
2032 $custom_language_path = $this->randomName(8);
2034 'source' => 'node/' . $node->nid,
2035 'alias' => $custom_language_path,
2036 'language' => $langcode,
2038 $this->drupalPost($path, $edit, t('Save'));
2040 // Confirm English language path alias works.
2041 $this->drupalGet($english_path);
2042 $this->assertText($node->title, 'English alias works.');
2044 // Confirm custom language path alias works.
2045 $this->drupalGet($prefix . '/' . $custom_language_path);
2046 $this->assertText($node->title, 'Custom language alias works.');
2048 // Create a custom path.
2049 $custom_path = $this->randomName(8);
2051 // Check priority of language for alias by source path.
2053 'source' => 'node/' . $node->nid,
2054 'alias' => $custom_path,
2055 'language' => LANGUAGE_NONE,
2058 $lookup_path = drupal_lookup_path('alias', 'node/' . $node->nid, 'en');
2059 $this->assertEqual($english_path, $lookup_path, 'English language alias has priority.');
2060 // Same check for language 'xx'.
2061 $lookup_path = drupal_lookup_path('alias', 'node/' . $node->nid, $prefix);
2062 $this->assertEqual($custom_language_path, $lookup_path, 'Custom language alias has priority.');
2065 // Create language nodes to check priority of aliases.
2066 $first_node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1));
2067 $second_node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1));
2069 // Assign a custom path alias to the first node with the English language.
2071 'source' => 'node/' . $first_node->nid,
2072 'alias' => $custom_path,
2077 // Assign a custom path alias to second node with LANGUAGE_NONE.
2079 'source' => 'node/' . $second_node->nid,
2080 'alias' => $custom_path,
2081 'language' => LANGUAGE_NONE,
2085 // Test that both node titles link to our path alias.
2086 $this->drupalGet('<front>');
2087 $custom_path_url = base_path() . (variable_get('clean_url', 0) ? $custom_path : '?q=' . $custom_path);
2088 $elements = $this->xpath('//a[@href=:href and .=:title]', array(':href' => $custom_path_url, ':title' => $first_node->title));
2089 $this->assertTrue(!empty($elements), 'First node links to the path alias.');
2090 $elements = $this->xpath('//a[@href=:href and .=:title]', array(':href' => $custom_path_url, ':title' => $second_node->title));
2091 $this->assertTrue(!empty($elements), 'Second node links to the path alias.');
2093 // Confirm that the custom path leads to the first node.
2094 $this->drupalGet($custom_path);
2095 $this->assertText($first_node->title, 'Custom alias returns first node.');
2097 // Confirm that the custom path with prefix leads to the second node.
2098 $this->drupalGet($prefix . '/' . $custom_path);
2099 $this->assertText($second_node->title, 'Custom alias with prefix returns second node.');
2104 * Functional tests for multilingual support on nodes.
2106 class LocaleContentFunctionalTest extends DrupalWebTestCase {
2107 public static function getInfo() {
2109 'name' => 'Content language settings',
2110 'description' => 'Checks you can enable multilingual support on content types and configure a language for a node.',
2111 'group' => 'Locale',
2116 parent::setUp('locale');
2120 * Verifies that machine name fields are always LTR.
2122 function testMachineNameLTR() {
2123 // User to add and remove language.
2124 $admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages'));
2127 $this->drupalLogin($admin_user);
2129 // Verify that the machine name field is LTR for a new content type.
2130 $this->drupalGet('admin/structure/types/add');
2131 $this->assertFieldByXpath('//input[@name="type" and @dir="ltr"]', NULL, 'The machine name field is LTR when no additional language is configured.');
2133 // Install the Arabic language (which is RTL) and configure as the default.
2135 $edit['langcode'] = 'ar';
2136 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
2139 $edit['site_default'] = 'ar';
2140 $this->drupalPost(NULL, $edit, t('Save configuration'));
2142 // Verify that the machine name field is still LTR for a new content type.
2143 $this->drupalGet('admin/structure/types/add');
2144 $this->assertFieldByXpath('//input[@name="type" and @dir="ltr"]', NULL, 'The machine name field is LTR when the default language is RTL.');
2148 * Test if a content type can be set to multilingual and language setting is
2149 * present on node add and edit forms.
2151 function testContentTypeLanguageConfiguration() {
2154 // User to add and remove language.
2155 $admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages'));
2156 // User to create a node.
2157 $web_user = $this->drupalCreateUser(array('create article content', 'create page content', 'edit any page content'));
2159 // Add custom language.
2160 $this->drupalLogin($admin_user);
2161 // Code for the language.
2163 // The English name for the language.
2164 $name = $this->randomName(16);
2165 // The native name for the language.
2166 $native = $this->randomName(16);
2167 // The domain prefix.
2168 $prefix = $langcode;
2170 'langcode' => $langcode,
2172 'native' => $native,
2173 'prefix' => $prefix,
2176 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
2178 // Add disabled custom language.
2179 // Code for the language.
2180 $langcode_disabled = 'xx-yy';
2181 // The English name for the language.
2182 $name_disabled = $this->randomName(16);
2183 // The native name for the language.
2184 $native_disabled = $this->randomName(16);
2185 // The domain prefix.
2186 $prefix_disabled = $langcode_disabled;
2188 'langcode' => $langcode_disabled,
2189 'name' => $name_disabled,
2190 'native' => $native_disabled,
2191 'prefix' => $prefix_disabled,
2194 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
2195 // Disable second custom language.
2196 $path = 'admin/config/regional/language';
2198 'enabled[' . $langcode_disabled . ']' => FALSE,
2200 $this->drupalPost($path, $edit, t('Save configuration'));
2202 // Set "Basic page" content type to use multilingual support.
2203 $this->drupalGet('admin/structure/types/manage/page');
2204 $this->assertText(t('Multilingual support'), 'Multilingual support fieldset present on content type configuration form.');
2206 'language_content_type' => 1,
2208 $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type'));
2209 $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), 'Basic page content type has been updated.');
2210 $this->drupalLogout();
2212 // Verify language selection is not present on add article form.
2213 $this->drupalLogin($web_user);
2214 $this->drupalGet('node/add/article');
2215 // Verify language select list is not present.
2216 $this->assertNoFieldByName('language', NULL, 'Language select not present on add article form.');
2218 // Verify language selection appears on add "Basic page" form.
2219 $this->drupalGet('node/add/page');
2220 // Verify language select list is present.
2221 $this->assertFieldByName('language', NULL, 'Language select present on add Basic page form.');
2222 // Ensure enabled language appears.
2223 $this->assertText($name, 'Enabled language present.');
2224 // Ensure disabled language doesn't appear.
2225 $this->assertNoText($name_disabled, 'Disabled language not present.');
2227 // Create "Basic page" content.
2228 $node_title = $this->randomName();
2229 $node_body = $this->randomName();
2232 'title' => $node_title,
2233 'body' => array($langcode => array(array('value' => $node_body))),
2234 'language' => $langcode,
2236 $node = $this->drupalCreateNode($edit);
2237 // Edit the content and ensure correct language is selected.
2238 $path = 'node/' . $node->nid . '/edit';
2239 $this->drupalGet($path);
2240 $this->assertRaw('<option value="' . $langcode . '" selected="selected">' . $name . '</option>', 'Correct language selected.');
2241 // Ensure we can change the node language.
2245 $this->drupalPost($path, $edit, t('Save'));
2246 $this->assertRaw(t('%title has been updated.', array('%title' => $node_title)), 'Basic page content updated.');
2248 $this->drupalLogout();
2252 * Verifies that nodes may be created with different languages.
2254 function testNodeCreationWithLanguage() {
2255 // Create an admin user and log them in.
2257 // Standard node permissions.
2258 'create page content',
2259 'administer content types',
2261 'bypass node access',
2263 'administer languages',
2265 $web_user = $this->drupalCreateUser($perms);
2266 $this->drupalLogin($web_user);
2268 // Create some test nodes using different langcodes.
2269 foreach (array(LANGUAGE_NONE, 'en', 'fr') as $langcode) {
2273 'language' => $langcode,
2275 $node = $this->drupalCreateNode($node_args);
2276 $node_reloaded = node_load($node->nid, NULL, TRUE);
2277 $this->assertEqual($node_reloaded->language, $langcode, format_string('The language code of the node was successfully set to @langcode.', array('@langcode' => $langcode)));
2284 * Test UI language negotiation
2285 * 1. URL (PATH) > DEFAULT
2286 * UI Language base on URL prefix, browser language preference has no
2289 * UI in site default language
2290 * zh-hans/admin/config
2292 * blah-blah/admin/config
2294 * 2. URL (PATH) > BROWSER > DEFAULT
2296 * UI in user's browser language preference if the site has that
2297 * language enabled, if not, the default language
2298 * zh-hans/admin/config
2300 * blah-blah/admin/config
2302 * 3. URL (DOMAIN) > DEFAULT
2303 * http://example.com/admin/config
2304 * UI language in site default
2305 * http://example.cn/admin/config
2306 * UI language in Chinese
2308 class LocaleUILanguageNegotiationTest extends DrupalWebTestCase {
2309 public static function getInfo() {
2311 'name' => 'UI language negotiation',
2312 'description' => 'Test UI language switching by URL path prefix and domain.',
2313 'group' => 'Locale',
2318 parent::setUp('locale', 'locale_test');
2319 require_once DRUPAL_ROOT . '/includes/language.inc';
2320 drupal_load('module', 'locale');
2321 $admin_user = $this->drupalCreateUser(array('administer languages', 'translate interface', 'access administration pages', 'administer blocks'));
2322 $this->drupalLogin($admin_user);
2326 * Tests for language switching by URL path.
2328 function testUILanguageNegotiation() {
2329 // A few languages to switch to.
2330 // This one is unknown, should get the default lang version.
2331 $language_unknown = 'blah-blah';
2332 // For testing browser lang preference.
2333 $language_browser_fallback = 'vi';
2334 // For testing path prefix.
2335 $language = 'zh-hans';
2336 // For setting browser language preference to 'vi'.
2337 $http_header_browser_fallback = array("Accept-Language: $language_browser_fallback;q=1");
2338 // For setting browser language preference to some unknown.
2339 $http_header_blah = array("Accept-Language: blah;q=1");
2341 // This domain should switch the UI to Chinese.
2342 $language_domain = 'example.cn';
2344 // Setup the site languages by installing two languages.
2345 require_once DRUPAL_ROOT . '/includes/locale.inc';
2346 locale_add_language($language_browser_fallback);
2347 locale_add_language($language);
2349 // We will look for this string in the admin/config screen to see if the
2350 // corresponding translated string is shown.
2351 $default_string = 'Configure languages for content and the user interface';
2353 // Set the default language in order for the translated string to be registered
2354 // into database when seen by t(). Without doing this, our target string
2355 // is for some reason not found when doing translate search. This might
2357 drupal_static_reset('language_list');
2358 $languages = language_list('enabled');
2359 variable_set('language_default', $languages[1]['vi']);
2360 // First visit this page to make sure our target string is searchable.
2361 $this->drupalGet('admin/config');
2362 // Now the t()'ed string is in db so switch the language back to default.
2363 variable_del('language_default');
2365 // Translate the string.
2366 $language_browser_fallback_string = "In $language_browser_fallback In $language_browser_fallback In $language_browser_fallback";
2367 $language_string = "In $language In $language In $language";
2368 // Do a translate search of our target string.
2369 $edit = array( 'string' => $default_string);
2370 $this->drupalPost('admin/config/regional/translate/translate', $edit, t('Filter'));
2371 // Should find the string and now click edit to post translated string.
2372 $this->clickLink('edit');
2374 "translations[$language_browser_fallback]" => $language_browser_fallback_string,
2375 "translations[$language]" => $language_string,
2377 $this->drupalPost(NULL, $edit, t('Save translations'));
2379 // Configure URL language rewrite.
2380 variable_set('locale_language_negotiation_url_type', LANGUAGE_TYPE_INTERFACE);
2383 // Default, browser preference should have no influence.
2385 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT),
2386 'path' => 'admin/config',
2387 'expect' => $default_string,
2388 'expected_provider' => LANGUAGE_NEGOTIATION_DEFAULT,
2389 'http_header' => $http_header_browser_fallback,
2390 'message' => 'URL (PATH) > DEFAULT: no language prefix, UI language is default and the browser language preference setting is not used.',
2394 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT),
2395 'path' => "$language/admin/config",
2396 'expect' => $language_string,
2397 'expected_provider' => LOCALE_LANGUAGE_NEGOTIATION_URL,
2398 'http_header' => $http_header_browser_fallback,
2399 'message' => 'URL (PATH) > DEFAULT: with language prefix, UI language is switched based on path prefix',
2401 // Default, go by browser preference.
2403 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_BROWSER),
2404 'path' => 'admin/config',
2405 'expect' => $language_browser_fallback_string,
2406 'expected_provider' => LOCALE_LANGUAGE_NEGOTIATION_BROWSER,
2407 'http_header' => $http_header_browser_fallback,
2408 'message' => 'URL (PATH) > BROWSER: no language prefix, UI language is determined by browser language preference',
2410 // Prefix, switch to the language.
2412 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_BROWSER),
2413 'path' => "$language/admin/config",
2414 'expect' => $language_string,
2415 'expected_provider' => LOCALE_LANGUAGE_NEGOTIATION_URL,
2416 'http_header' => $http_header_browser_fallback,
2417 'message' => 'URL (PATH) > BROWSER: with langage prefix, UI language is based on path prefix',
2419 // Default, browser language preference is not one of site's lang.
2421 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LOCALE_LANGUAGE_NEGOTIATION_BROWSER, LANGUAGE_NEGOTIATION_DEFAULT),
2422 'path' => 'admin/config',
2423 'expect' => $default_string,
2424 'expected_provider' => LANGUAGE_NEGOTIATION_DEFAULT,
2425 'http_header' => $http_header_blah,
2426 'message' => 'URL (PATH) > BROWSER > DEFAULT: no language prefix and browser language preference set to unknown language should use default language',
2430 foreach ($tests as $test) {
2431 $this->runTest($test);
2434 // Unknown language prefix should return 404.
2435 variable_set('language_negotiation_' . LANGUAGE_TYPE_INTERFACE, locale_language_negotiation_info());
2436 $this->drupalGet("$language_unknown/admin/config", array(), $http_header_browser_fallback);
2437 $this->assertResponse(404, "Unknown language path prefix should return 404");
2439 // Setup for domain negotiation, first configure the language to have domain
2440 // URL. We use HTTPS and a port to make sure that only the domain name is used.
2441 $edit = array('prefix' => '', 'domain' => "https://$language_domain:99");
2442 $this->drupalPost("admin/config/regional/language/edit/$language", $edit, t('Save language'));
2443 // Set the site to use domain language negotiation.
2446 // Default domain, browser preference should have no influence.
2448 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT),
2449 'locale_language_negotiation_url_part' => LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN,
2450 'path' => 'admin/config',
2451 'expect' => $default_string,
2452 'expected_provider' => LANGUAGE_NEGOTIATION_DEFAULT,
2453 'http_header' => $http_header_browser_fallback,
2454 'message' => 'URL (DOMAIN) > DEFAULT: default domain should get default language',
2456 // Language domain specific URL, we set the $_SERVER['HTTP_HOST'] in
2457 // locale_test.module hook_boot() to simulate this.
2459 'language_negotiation' => array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT),
2460 'locale_language_negotiation_url_part' => LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN,
2461 'locale_test_domain' => $language_domain . ':88',
2462 'path' => 'admin/config',
2463 'expect' => $language_string,
2464 'expected_provider' => LOCALE_LANGUAGE_NEGOTIATION_URL,
2465 'http_header' => $http_header_browser_fallback,
2466 'message' => 'URL (DOMAIN) > DEFAULT: domain example.cn should switch to Chinese',
2470 foreach ($tests as $test) {
2471 $this->runTest($test);
2475 private function runTest($test) {
2476 if (!empty($test['language_negotiation'])) {
2477 $negotiation = array_flip($test['language_negotiation']);
2478 language_negotiation_set(LANGUAGE_TYPE_INTERFACE, $negotiation);
2480 if (!empty($test['locale_language_negotiation_url_part'])) {
2481 variable_set('locale_language_negotiation_url_part', $test['locale_language_negotiation_url_part']);
2483 if (!empty($test['locale_test_domain'])) {
2484 variable_set('locale_test_domain', $test['locale_test_domain']);
2486 $this->drupalGet($test['path'], array(), $test['http_header']);
2487 $this->assertText($test['expect'], $test['message']);
2488 $this->assertText(t('Language negotiation provider: @name', array('@name' => $test['expected_provider'])));
2492 * Test URL language detection when the requested URL has no language.
2494 function testUrlLanguageFallback() {
2495 // Add the Italian language.
2496 $language_browser_fallback = 'it';
2497 locale_add_language($language_browser_fallback);
2498 $languages = language_list();
2500 // Enable the path prefix for the default language: this way any unprefixed
2501 // URL must have a valid fallback value.
2502 $edit = array('prefix' => 'en');
2503 $this->drupalPost('admin/config/regional/language/edit/en', $edit, t('Save language'));
2505 // Enable browser and URL language detection.
2507 'language[enabled][locale-browser]' => TRUE,
2508 'language[enabled][locale-url]' => TRUE,
2509 'language[weight][locale-browser]' => -8,
2510 'language[weight][locale-url]' => -10,
2512 $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings'));
2513 $this->drupalGet('admin/config/regional/language/configure');
2515 // Enable the language switcher block.
2516 $edit = array('blocks[locale_language][region]' => 'sidebar_first');
2517 $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
2519 // Access the front page without specifying any valid URL language prefix
2520 // and having as browser language preference a non-default language.
2521 $http_header = array("Accept-Language: $language_browser_fallback;q=1");
2522 $this->drupalGet('', array(), $http_header);
2524 // Check that the language switcher active link matches the given browser
2526 $args = array(':url' => base_path() . (!empty($GLOBALS['conf']['clean_url']) ? $language_browser_fallback : "?q=$language_browser_fallback"));
2527 $fields = $this->xpath('//div[@id="block-locale-language"]//a[@class="language-link active" and @href=:url]', $args);
2528 $this->assertTrue($fields[0] == $languages[$language_browser_fallback]->native, 'The browser language is the URL active language');
2530 // Check that URLs are rewritten using the given browser language.
2531 $fields = $this->xpath('//div[@id="site-name"]//a[@rel="home" and @href=:url]//span', $args);
2532 $this->assertTrue($fields[0] == 'Drupal', 'URLs are rewritten using the browser language.');
2536 * Tests url() when separate domains are used for multiple languages.
2538 function testLanguageDomain() {
2539 // Add the Italian language, without protocol.
2541 locale_add_language($langcode, 'Italian', 'Italian', LANGUAGE_LTR, 'it.example.com', '', TRUE, FALSE);
2543 // Add the French language, with protocol.
2545 locale_add_language($langcode, 'French', 'French', LANGUAGE_LTR, 'http://fr.example.com', '', TRUE, FALSE);
2547 // Enable language URL detection.
2548 $negotiation = array_flip(array(LOCALE_LANGUAGE_NEGOTIATION_URL, LANGUAGE_NEGOTIATION_DEFAULT));
2549 language_negotiation_set(LANGUAGE_TYPE_INTERFACE, $negotiation);
2551 variable_set('locale_language_negotiation_url_part', 1);
2554 $languages = language_list();
2556 foreach (array('it', 'fr') as $langcode) {
2557 // Build the link we're going to test based on the clean URL setting.
2558 $link = (!empty($GLOBALS['conf']['clean_url'])) ? $langcode . '.example.com/admin' : $langcode . '.example.com/?q=admin';
2560 // Test URL in another language.
2561 // Base path gives problems on the testbot, so $correct_link is hard-coded.
2562 // @see UrlAlterFunctionalTest::assertUrlOutboundAlter (path.test).
2563 $url = url('admin', array('language' => $languages[$langcode]));
2564 $url_scheme = ($is_https) ? 'https://' : 'http://';
2565 $correct_link = $url_scheme . $link;
2566 $this->assertTrue($url == $correct_link, format_string('The url() function returns the right url (@url) in accordance with the chosen language', array('@url' => $url . " == " . $correct_link)));
2568 // Test HTTPS via options.
2569 variable_set('https', TRUE);
2570 $url = url('admin', array('https' => TRUE, 'language' => $languages[$langcode]));
2571 $correct_link = 'https://' . $link;
2572 $this->assertTrue($url == $correct_link, format_string('The url() function returns the right https url (via options) (@url) in accordance with the chosen language', array('@url' => $url . " == " . $correct_link)));
2573 variable_set('https', FALSE);
2575 // Test HTTPS via current URL scheme.
2576 $temp_https = $is_https;
2578 $url = url('admin', array('language' => $languages[$langcode]));
2579 $correct_link = 'https://' . $link;
2580 $this->assertTrue($url == $correct_link, format_string('The url() function returns the right url (via current url scheme) (@url) in accordance with the chosen language', array('@url' => $url . " == " . $correct_link)));
2581 $is_https = $temp_https;
2587 * Test that URL rewriting works as expected.
2589 class LocaleUrlRewritingTest extends DrupalWebTestCase {
2590 public static function getInfo() {
2592 'name' => 'URL rewriting',
2593 'description' => 'Test that URL rewriting works as expected.',
2594 'group' => 'Locale',
2599 parent::setUp('locale');
2601 // Create and login user.
2602 $this->web_user = $this->drupalCreateUser(array('administer languages', 'access administration pages'));
2603 $this->drupalLogin($this->web_user);
2605 // Install French language.
2607 $edit['langcode'] = 'fr';
2608 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
2610 // Install Italian language.
2612 $edit['langcode'] = 'it';
2613 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
2615 // Disable Italian language.
2616 $edit = array('enabled[it]' => FALSE);
2617 $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration'));
2619 // Enable URL language detection and selection.
2620 $edit = array('language[enabled][locale-url]' => 1);
2621 $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings'));
2623 // Reset static caching.
2624 drupal_static_reset('language_list');
2625 drupal_static_reset('locale_url_outbound_alter');
2626 drupal_static_reset('locale_language_url_rewrite_url');
2630 * Check that disabled or non-installed languages are not considered.
2632 function testUrlRewritingEdgeCases() {
2633 // Check URL rewriting with a disabled language.
2634 $languages = language_list();
2635 $this->checkUrl($languages['it'], 'Path language is ignored if language is disabled.', 'URL language negotiation does not work with disabled languages');
2637 // Check URL rewriting with a non-installed language.
2638 $non_existing = language_default();
2639 $non_existing->language = $this->randomName();
2640 $non_existing->prefix = $this->randomName();
2641 $this->checkUrl($non_existing, 'Path language is ignored if language is not installed.', 'URL language negotiation does not work with non-installed languages');
2645 * Check URL rewriting for the given language.
2647 * The test is performed with a fixed URL (the default front page) to simply
2648 * check that language prefixes are not added to it and that the prefixed URL
2649 * is actually not working.
2651 * @param string $language
2652 * The language prefix, e.g. 'es'.
2653 * @param string $message1
2654 * Message to display in assertion that language prefixes are not added.
2655 * @param string $message2
2656 * The message to display confirming prefixed URL is not working.
2658 private function checkUrl($language, $message1, $message2) {
2659 $options = array('language' => $language);
2660 $base_path = trim(base_path(), '/');
2661 $rewritten_path = trim(str_replace(array('?q=', $base_path), '', url('node', $options)), '/');
2662 $segments = explode('/', $rewritten_path, 2);
2663 $prefix = $segments[0];
2664 $path = isset($segments[1]) ? $segments[1] : $prefix;
2665 // If the rewritten URL has not a language prefix we pick the right one from
2666 // the language object so we can always check the prefixed URL.
2667 if ($this->assertNotEqual($language->prefix, $prefix, $message1)) {
2668 $prefix = $language->prefix;
2670 $this->drupalGet("$prefix/$path");
2671 $this->assertResponse(404, $message2);
2675 * Check URL rewriting when using a domain name and a non-standard port.
2677 function testDomainNameNegotiationPort() {
2678 $language_domain = 'example.fr';
2680 'locale_language_negotiation_url_part' => 1,
2682 $this->drupalPost('admin/config/regional/language/configure/url', $edit, t('Save configuration'));
2685 'domain' => $language_domain
2687 $this->drupalPost('admin/config/regional/language/edit/fr', $edit, t('Save language'));
2689 // Enable domain configuration.
2690 variable_set('locale_language_negotiation_url_part', LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN);
2692 // Reset static caching.
2693 drupal_static_reset('language_list');
2694 drupal_static_reset('language_url_outbound_alter');
2695 drupal_static_reset('language_url_rewrite_url');
2697 // In case index.php is part of the URLs, we need to adapt the asserted
2699 $index_php = strpos(url('', array('absolute' => TRUE)), 'index.php') !== FALSE;
2701 // Remember current HTTP_HOST.
2702 $http_host = $_SERVER['HTTP_HOST'];
2704 // Fake a different port.
2705 $_SERVER['HTTP_HOST'] .= ':88';
2707 // Create an absolute French link.
2708 $languages = language_list();
2709 $language = $languages['fr'];
2710 $url = url('', array(
2712 'language' => $language
2715 $expected = 'http://example.fr:88/';
2716 $expected .= $index_php ? 'index.php/' : '';
2718 $this->assertEqual($url, $expected, 'The right port is used.');
2720 // If we set the port explicitly in url(), it should not be overriden.
2721 $url = url('', array(
2723 'language' => $language,
2724 'base_url' => $GLOBALS['base_url'] . ':90',
2727 $expected = 'http://example.fr:90/';
2728 $expected .= $index_php ? 'index.php/' : '';
2730 $this->assertEqual($url, $expected, 'A given port is not overriden.');
2732 // Restore HTTP_HOST.
2733 $_SERVER['HTTP_HOST'] = $http_host;
2738 * Functional test for multilingual fields.
2740 class LocaleMultilingualFieldsFunctionalTest extends DrupalWebTestCase {
2741 public static function getInfo() {
2743 'name' => 'Multilingual fields',
2744 'description' => 'Test multilingual support for fields.',
2745 'group' => 'Locale',
2750 parent::setUp('locale');
2752 $admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages', 'create page content', 'edit own page content'));
2753 $this->drupalLogin($admin_user);
2755 // Add a new language.
2756 require_once DRUPAL_ROOT . '/includes/locale.inc';
2757 locale_add_language('it', 'Italian', 'Italiano', LANGUAGE_LTR, '', '', TRUE, FALSE);
2759 // Enable URL language detection and selection.
2760 $edit = array('language[enabled][locale-url]' => '1');
2761 $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings'));
2763 // Set "Basic page" content type to use multilingual support.
2765 'language_content_type' => 1,
2767 $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type'));
2768 $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), 'Basic page content type has been updated.');
2770 // Make node body translatable.
2771 $field = field_info_field('body');
2772 $field['translatable'] = TRUE;
2773 field_update_field($field);
2777 * Test if field languages are correctly set through the node form.
2779 function testMultilingualNodeForm() {
2780 // Create "Basic page" content.
2781 $langcode = LANGUAGE_NONE;
2782 $title_key = "title";
2783 $title_value = $this->randomName(8);
2784 $body_key = "body[$langcode][0][value]";
2785 $body_value = $this->randomName(16);
2787 // Create node to edit.
2789 $edit[$title_key] = $title_value;
2790 $edit[$body_key] = $body_value;
2791 $edit['language'] = 'en';
2792 $this->drupalPost('node/add/page', $edit, t('Save'));
2794 // Check that the node exists in the database.
2795 $node = $this->drupalGetNodeByTitle($edit[$title_key]);
2796 $this->assertTrue($node, 'Node found in database.');
2798 $assert = isset($node->body['en']) && !isset($node->body[LANGUAGE_NONE]) && $node->body['en'][0]['value'] == $body_value;
2799 $this->assertTrue($assert, 'Field language correctly set.');
2801 // Change node language.
2802 $this->drupalGet("node/$node->nid/edit");
2804 $title_key => $this->randomName(8),
2807 $this->drupalPost(NULL, $edit, t('Save'));
2808 $node = $this->drupalGetNodeByTitle($edit[$title_key]);
2809 $this->assertTrue($node, 'Node found in database.');
2811 $assert = isset($node->body['it']) && !isset($node->body['en']) && $node->body['it'][0]['value'] == $body_value;
2812 $this->assertTrue($assert, 'Field language correctly changed.');
2814 // Enable content language URL detection.
2815 language_negotiation_set(LANGUAGE_TYPE_CONTENT, array(LOCALE_LANGUAGE_NEGOTIATION_URL => 0));
2817 // Test multilingual field language fallback logic.
2818 $this->drupalGet("it/node/$node->nid");
2819 $this->assertRaw($body_value, 'Body correctly displayed using Italian as requested language');
2821 $this->drupalGet("node/$node->nid");
2822 $this->assertRaw($body_value, 'Body correctly displayed using English as requested language');
2826 * Test multilingual field display settings.
2828 function testMultilingualDisplaySettings() {
2829 // Create "Basic page" content.
2830 $langcode = LANGUAGE_NONE;
2831 $title_key = "title";
2832 $title_value = $this->randomName(8);
2833 $body_key = "body[$langcode][0][value]";
2834 $body_value = $this->randomName(16);
2836 // Create node to edit.
2838 $edit[$title_key] = $title_value;
2839 $edit[$body_key] = $body_value;
2840 $edit['language'] = 'en';
2841 $this->drupalPost('node/add/page', $edit, t('Save'));
2843 // Check that the node exists in the database.
2844 $node = $this->drupalGetNodeByTitle($edit[$title_key]);
2845 $this->assertTrue($node, 'Node found in database.');
2847 // Check if node body is showed.
2848 $this->drupalGet("node/$node->nid");
2849 $body = $this->xpath('//div[@id=:id]//div[@property="content:encoded"]/p', array(':id' => 'node-' . $node->nid));
2850 $this->assertEqual(current($body), $node->body['en'][0]['value'], 'Node body is correctly showed.');
2855 * Functional tests for comment language.
2857 class LocaleCommentLanguageFunctionalTest extends DrupalWebTestCase {
2859 public static function getInfo() {
2861 'name' => 'Comment language',
2862 'description' => 'Tests for comment language.',
2863 'group' => 'Locale',
2868 parent::setUp('locale', 'locale_test');
2870 // Create and login user.
2871 $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'administer content types', 'administer comments', 'create article content'));
2872 $this->drupalLogin($admin_user);
2875 $edit = array('langcode' => 'fr');
2876 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
2878 // Set "Article" content type to use multilingual support.
2879 $edit = array('language_content_type' => 1);
2880 $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
2882 // Enable content language negotiation UI.
2883 variable_set('locale_test_content_language_type', TRUE);
2885 // Set interface language detection to user and content language detection
2886 // to URL. Disable inheritance from interface language to ensure content
2887 // language will fall back to the default language if no URL language can be
2890 'language[enabled][locale-user]' => TRUE,
2891 'language_content[enabled][locale-url]' => TRUE,
2892 'language_content[enabled][locale-interface]' => FALSE,
2894 $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings'));
2896 // Change user language preference, this way interface language is always
2897 // French no matter what path prefix the URLs have.
2898 $edit = array('language' => 'fr');
2899 $this->drupalPost("user/{$admin_user->uid}/edit", $edit, t('Save'));
2901 // Make comment body translatable.
2902 $field = field_info_field('comment_body');
2903 $field['translatable'] = TRUE;
2904 field_update_field($field);
2905 $this->assertTrue(field_is_translatable('comment', $field), 'Comment body is translatable.');
2909 * Test that comment language is properly set.
2911 function testCommentLanguage() {
2912 drupal_static_reset('language_list');
2914 // Create two nodes, one for english and one for french, and comment each
2915 // node using both english and french as content language by changing URL
2916 // language prefixes. Meanwhile interface language is always French, which
2917 // is the user language preference. This way we can ensure that node
2918 // language and interface language do not influence comment language, as
2919 // only content language has to.
2920 foreach (language_list() as $node_langcode => $node_language) {
2921 $language_none = LANGUAGE_NONE;
2923 // Create "Article" content.
2924 $title = $this->randomName();
2927 "body[$language_none][0][value]" => $this->randomName(),
2928 "language" => $node_langcode,
2930 $this->drupalPost("node/add/article", $edit, t('Save'));
2931 $node = $this->drupalGetNodeByTitle($title);
2933 foreach (language_list() as $langcode => $language) {
2934 // Post a comment with content language $langcode.
2935 $prefix = empty($language->prefix) ? '' : $language->prefix . '/';
2936 $comment_values[$node_langcode][$langcode] = $this->randomName();
2937 // Initially field form widgets have no language.
2939 'subject' => $this->randomName(),
2940 "comment_body[$language_none][0][value]" => $comment_values[$node_langcode][$langcode],
2942 $this->drupalPost("{$prefix}node/{$node->nid}", $edit, t('Preview'));
2943 // After the first submit the submitted entity language is taken into
2946 'subject' => $edit['subject'],
2947 "comment_body[$langcode][0][value]" => $comment_values[$node_langcode][$langcode],
2949 $this->drupalPost(NULL, $edit, t('Save'));
2951 // Check that comment language matches the current content language.
2952 $cid = db_select('comment', 'c')
2953 ->fields('c', array('cid'))
2954 ->condition('nid', $node->nid)
2955 ->orderBy('cid', 'DESC')
2959 $comment = comment_load($cid);
2960 $comment_langcode = entity_language('comment', $comment);
2961 $args = array('%node_language' => $node_langcode, '%comment_language' => $comment_langcode, '%langcode' => $langcode);
2962 $this->assertEqual($comment_langcode, $langcode, format_string('The comment posted with content language %langcode and belonging to the node with language %node_language has language %comment_language', $args));
2963 $this->assertEqual($comment->comment_body[$langcode][0]['value'], $comment_values[$node_langcode][$langcode], 'Comment body correctly stored.');
2967 // Check that comment bodies appear in the administration UI.
2968 $this->drupalGet('admin/content/comment');
2969 foreach ($comment_values as $node_values) {
2970 foreach ($node_values as $value) {
2971 $this->assertRaw($value);
2979 * Functional tests for localizing date formats.
2981 class LocaleDateFormatsFunctionalTest extends DrupalWebTestCase {
2983 public static function getInfo() {
2985 'name' => 'Localize date formats',
2986 'description' => 'Tests for the localization of date formats.',
2987 'group' => 'Locale',
2992 parent::setUp('locale');
2994 // Create and login user.
2995 $admin_user = $this->drupalCreateUser(array('administer site configuration', 'administer languages', 'access administration pages', 'create article content'));
2996 $this->drupalLogin($admin_user);
3000 * Functional tests for localizing date formats.
3002 function testLocalizeDateFormats() {
3007 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
3009 // Set language negotiation.
3010 $language_type = LANGUAGE_TYPE_INTERFACE;
3012 "{$language_type}[enabled][locale-url]" => TRUE,
3014 $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings'));
3016 // Configure date formats.
3017 $this->drupalGet('admin/config/regional/date-time/locale');
3018 $this->assertText('Français', 'Configured languages appear.');
3020 'date_format_long' => 'd.m.Y - H:i',
3021 'date_format_medium' => 'd.m.Y - H:i',
3022 'date_format_short' => 'd.m.Y - H:i',
3024 $this->drupalPost('admin/config/regional/date-time/locale/fr/edit', $edit, t('Save configuration'));
3025 $this->assertText(t('Configuration saved.'), 'French date formats updated.');
3027 'date_format_long' => 'j M Y - g:ia',
3028 'date_format_medium' => 'j M Y - g:ia',
3029 'date_format_short' => 'j M Y - g:ia',
3031 $this->drupalPost('admin/config/regional/date-time/locale/en/edit', $edit, t('Save configuration'));
3032 $this->assertText(t('Configuration saved.'), 'English date formats updated.');
3034 // Create node content.
3035 $node = $this->drupalCreateNode(array('type' => 'article'));
3037 // Configure format for the node posted date changes with the language.
3038 $this->drupalGet('node/' . $node->nid);
3039 $english_date = format_date($node->created, 'custom', 'j M Y');
3040 $this->assertText($english_date, 'English date format appears');
3041 $this->drupalGet('fr/node/' . $node->nid);
3042 $french_date = format_date($node->created, 'custom', 'd.m.Y');
3043 $this->assertText($french_date, 'French date format appears');
3048 * Functional test for language types/negotiation info.
3050 class LocaleLanguageNegotiationInfoFunctionalTest extends DrupalWebTestCase {
3052 public static function getInfo() {
3054 'name' => 'Language negotiation info',
3055 'description' => 'Tests alterations to language types/negotiation info.',
3056 'group' => 'Locale',
3061 parent::setUp('locale');
3062 require_once DRUPAL_ROOT .'/includes/language.inc';
3063 $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'view the administration theme'));
3064 $this->drupalLogin($admin_user);
3065 $this->drupalPost('admin/config/regional/language/add', array('langcode' => 'it'), t('Add language'));
3069 * Tests alterations to language types/negotiation info.
3071 function testInfoAlterations() {
3072 // Enable language type/negotiation info alterations.
3073 variable_set('locale_test_language_types', TRUE);
3074 variable_set('locale_test_language_negotiation_info', TRUE);
3075 $this->languageNegotiationUpdate();
3077 // Check that fixed language types are properly configured without the need
3078 // of saving the language negotiation settings.
3079 $this->checkFixedLanguageTypes();
3081 // Make the content language type configurable by updating the language
3082 // negotiation settings with the proper flag enabled.
3083 variable_set('locale_test_content_language_type', TRUE);
3084 $this->languageNegotiationUpdate();
3085 $type = LANGUAGE_TYPE_CONTENT;
3086 $language_types = variable_get('language_types', drupal_language_types());
3087 $this->assertTrue($language_types[$type], 'Content language type is configurable.');
3089 // Enable some core and custom language providers. The test language type is
3090 // supposed to be configurable.
3091 $test_type = 'test_language_type';
3092 $provider = LOCALE_LANGUAGE_NEGOTIATION_INTERFACE;
3093 $test_provider = 'test_language_provider';
3094 $form_field = $type . '[enabled]['. $provider .']';
3096 $form_field => TRUE,
3097 $type . '[enabled][' . $test_provider . ']' => TRUE,
3098 $test_type . '[enabled][' . $test_provider . ']' => TRUE,
3100 $this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings'));
3102 // Remove the interface language provider by updating the language
3103 // negotiation settings with the proper flag enabled.
3104 variable_set('locale_test_language_negotiation_info_alter', TRUE);
3105 $this->languageNegotiationUpdate();
3106 $negotiation = variable_get("language_negotiation_$type", array());
3107 $this->assertFalse(isset($negotiation[$provider]), 'Interface language provider removed from the stored settings.');
3108 $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, 'Interface language provider unavailable.');
3110 // Check that type-specific language providers can be assigned only to the
3111 // corresponding language types.
3112 foreach (language_types_configurable() as $type) {
3113 $form_field = $type . '[enabled][test_language_provider_ts]';
3114 if ($type == $test_type) {
3115 $this->assertFieldByXPath("//input[@name=\"$form_field\"]", NULL, format_string('Type-specific test language provider available for %type.', array('%type' => $type)));
3118 $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, format_string('Type-specific test language provider unavailable for %type.', array('%type' => $type)));
3122 // Check language negotiation results.
3123 $this->drupalGet('');
3124 $last = variable_get('locale_test_language_negotiation_last', array());
3125 foreach (language_types() as $type) {
3126 $langcode = $last[$type];
3127 $value = $type == LANGUAGE_TYPE_CONTENT || strpos($type, 'test') !== FALSE ? 'it' : 'en';
3128 $this->assertEqual($langcode, $value, format_string('The negotiated language for %type is %language', array('%type' => $type, '%language' => $langcode)));
3131 // Disable locale_test and check that everything is set back to the original
3133 $this->languageNegotiationUpdate('disable');
3135 // Check that only the core language types are available.
3136 foreach (language_types() as $type) {
3137 $this->assertTrue(strpos($type, 'test') === FALSE, format_string('The %type language is still available', array('%type' => $type)));
3140 // Check that fixed language types are properly configured, even those
3141 // previously set to configurable.
3142 $this->checkFixedLanguageTypes();
3144 // Check that unavailable language providers are not present in the
3145 // negotiation settings.
3146 $negotiation = variable_get("language_negotiation_$type", array());
3147 $this->assertFalse(isset($negotiation[$test_provider]), 'The disabled test language provider is not part of the content language negotiation settings.');
3149 // Check that configuration page presents the correct options and settings.
3150 $this->assertNoRaw(t('Test language detection'), 'No test language type configuration available.');
3151 $this->assertNoRaw(t('This is a test language provider'), 'No test language provider available.');
3155 * Update language types/negotiation information.
3157 * Manually invoke locale_modules_enabled()/locale_modules_disabled() since
3158 * they would not be invoked after enabling/disabling locale_test the first
3161 private function languageNegotiationUpdate($op = 'enable') {
3162 static $last_op = NULL;
3163 $modules = array('locale_test');
3165 // Enable/disable locale_test only if we did not already before.
3166 if ($last_op != $op) {
3167 $function = "module_{$op}";
3168 $function($modules);
3169 // Reset hook implementation cache.
3170 module_implements(NULL, FALSE, TRUE);
3173 drupal_static_reset('language_types_info');
3174 drupal_static_reset('language_negotiation_info');
3175 $function = "locale_modules_{$op}d";
3176 if (function_exists($function)) {
3177 $function($modules);
3180 $this->drupalGet('admin/config/regional/language/configure');
3184 * Check that language negotiation for fixed types matches the stored one.
3186 private function checkFixedLanguageTypes() {
3187 drupal_static_reset('language_types_info');
3188 foreach (language_types_info() as $type => $info) {
3189 if (isset($info['fixed'])) {
3190 $negotiation = variable_get("language_negotiation_$type", array());
3191 $equal = count($info['fixed']) == count($negotiation);
3192 while ($equal && list($id) = each($negotiation)) {
3193 list(, $info_id) = each($info['fixed']);
3194 $equal = $info_id == $id;
3196 $this->assertTrue($equal, format_string('language negotiation for %type is properly set up', array('%type' => $type)));
3203 * Functional tests for CSS alter functions.
3205 class LocaleCSSAlterTest extends DrupalWebTestCase {
3206 public static function getInfo() {
3208 'name' => 'CSS altering',
3209 'description' => 'Test CSS alter functions.',
3210 'group' => 'Locale',
3215 parent::setUp('locale');
3219 * Verifies that -rtl.css file is added directly after LTR .css file.
3221 function testCSSFilesOrderInRTLMode() {
3224 // User to add and remove language.
3225 $admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages'));
3228 $this->drupalLogin($admin_user);
3230 // Install the Arabic language (which is RTL) and configure as the default.
3232 $edit['langcode'] = 'ar';
3233 $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
3236 $edit['site_default'] = 'ar';
3237 $this->drupalPost(NULL, $edit, t('Save configuration'));
3239 // Verify that the -rtl.css file is added directly after LTR file.
3240 $this->drupalGet('');
3241 $query_string = '?' . variable_get('css_js_query_string', '0');
3242 $this->assertRaw('@import url("' . $base_url . '/modules/system/system.base.css' . $query_string . '");' . "\n" . '@import url("' . $base_url . '/modules/system/system.base-rtl.css' . $query_string . '");' . "\n", 'CSS: system.base-rtl.css is added directly after system.base.css.');
3243 $this->assertRaw('@import url("' . $base_url . '/modules/system/system.menus.css' . $query_string . '");' . "\n" . '@import url("' . $base_url . '/modules/system/system.menus-rtl.css' . $query_string . '");' . "\n", 'CSS: system.menus-rtl.css is added directly after system.menus.css.');
3244 $this->assertRaw('@import url("' . $base_url . '/modules/system/system.messages.css' . $query_string . '");' . "\n" . '@import url("' . $base_url . '/modules/system/system.messages-rtl.css' . $query_string . '");' . "\n", 'CSS: system.messages-rtl.css is added directly after system.messages.css.');
3249 * Tests locale translation safe string handling.
3251 class LocaleStringIsSafeTest extends DrupalWebTestCase {
3252 public static function getInfo() {
3254 'name' => 'Test if a string is safe',
3255 'description' => 'Tests locale translation safe string handling.',
3256 'group' => 'Locale',
3261 parent::setUp('locale');
3265 * Tests for locale_string_is_safe().
3267 public function testLocaleStringIsSafe() {
3268 // Check a translatable string without HTML.
3269 $string = 'Hello world!';
3270 $result = locale_string_is_safe($string);
3271 $this->assertTrue($result);
3273 // Check a translatable string which includes trustable HTML.
3274 $string = 'Hello <strong>world</strong>!';
3275 $result = locale_string_is_safe($string);
3276 $this->assertTrue($result);
3278 // Check an untranslatable string which includes untrustable HTML (according
3279 // to the locale_string_is_safe() function definition).
3280 $string = 'Hello <img src="world.png" alt="world" />!';
3281 $result = locale_string_is_safe($string);
3282 $this->assertFalse($result);
3284 // Check a translatable string which includes a token in an href attribute.
3285 $string = 'Hi <a href="[current-user:url]">user</a>';
3286 $result = locale_string_is_safe($string);
3287 $this->assertTrue($result);