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

3
4
/**
 * @file
5
 * Tests for locale.module.
6
 *
7
 * The test file includes:
8
9
10
11
12
13
14
15
16
17
 *  - a functional test for the language configuration forms;
 *  - functional tests for the translation functionalities, including searching;
 *  - a functional test for the PO files import feature, including validation;
 *  - functional tests for translations and templates export feature;
 *  - functional tests for the uninstall process;
 *  - a functional test for the language switching feature;
 *  - a functional test for a user's ability to change their default language;
 *  - a functional test for configuring a different path alias per language;
 *  - a functional test for configuring a different path alias per language;
 *  - a functional test for multilingual support by content type and on nodes.
18
 *  - a functional test for multilingual fields.
19
 *  - a functional test for comment language.
20
 *  - a functional test fot language types/negotiation info.
21
22
 */

23
24
25
26
27

/**
 * Functional tests for the language configuration forms.
 */
class LocaleConfigurationTest extends DrupalWebTestCase {
28
  public static function getInfo() {
29
    return array(
30
31
32
      'name' => 'Language configuration',
      'description' => 'Adds a new locale and tests changing its status and the default language.',
      'group' => 'Locale',
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
    );
  }

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

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

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

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

    // Add custom language.
    // Code for the language.
60
    $langcode = 'xx';
61
62
63
64
65
    // The English name for the language.
    $name = $this->randomName(16);
    // The native name for the language.
    $native = $this->randomName(16);
    // The domain prefix.
66
    $prefix = $langcode;
67
68
69
70
71
72
73
    $edit = array(
      'langcode' => $langcode,
      'name' => $name,
      'native' => $native,
      'prefix' => $prefix,
      'direction' => '0',
    );
74
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
75
76
77
78
79
    $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.'));
    $this->assertText($langcode, t('Language code found.'));
    $this->assertText($name, t('Name found.'));
    $this->assertText($native, t('Native found.'));
    $this->assertText($native, t('Test language added.'));
80
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', t('English is the default language.'));
85
86
87
88
    // Change the default language.
    $edit = array(
      'site_default' => $langcode,
    );
89
    $this->drupalPost(NULL, $edit, t('Save configuration'));
90
91
    $this->assertNoFieldChecked('edit-site-default-en', t('Default language updated.'));
    $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.'));
92

93
94
95
96
97
    // Check if a valid language prefix is added after changing the default
    // language.
    $this->drupalGet('admin/config/regional/language/edit/en');
    $this->assertFieldByXPath('//input[@name="prefix"]', 'en', t('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
101
    $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.'));
    $this->assertText(t('The default language cannot be deleted.'), t('Failed to delete the default language.'));
102
103
104
105
106
107

    // Check if we can disable a language.
    $edit = array(
      'enabled[en]' => FALSE,
    );
    $this->drupalPost($path, $edit, t('Save configuration'));
108
    $this->assertNoFieldChecked('edit-enabled-en', t('Language disabled.'));
109
110
111
112
113

    // Set disabled language to be the default and ensure it is re-enabled.
    $edit = array(
      'site_default' => 'en',
    );
114
    $this->drupalPost(NULL, $edit, t('Save configuration'));
115
    $this->assertFieldChecked('edit-enabled-en', t('Default language re-enabled.'));
116
117
118

    // Ensure 'edit' link works.
    $this->clickLink(t('edit'));
119
    $this->assertTitle(t('Edit language | Drupal'), t('Page title is "Edit language".'));
120
121
122
123
124
    // Edit a language.
    $name = $this->randomName(16);
    $edit = array(
      'name' => $name,
    );
125
    $this->drupalPost('admin/config/regional/language/edit/' . $langcode, $edit, t('Save language'));
126
127
    $this->assertRaw($name, t('The language has been updated.'));
    $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.'));
128
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'), t('"delete" link is correct.'));
133
    // Delete an enabled language.
134
    $this->drupalGet('admin/config/regional/language/delete/' . $langcode);
135
136
    // First test the 'cancel' link.
    $this->clickLink(t('Cancel'));
137
138
    $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.'));
    $this->assertRaw($name, t('The language was not deleted.'));
139
140
    // Delete the language for real. This a confirm form, we do not need any
    // fields changed.
141
    $this->drupalPost('admin/config/regional/language/delete/' . $langcode, array(), t('Delete'));
142
    // We need raw here because %locale will add HTML.
143
144
    $this->assertRaw(t('The language %locale has been removed.', array('%locale' => $name)), t('The test language has been removed.'));
    $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.'));
145
146
    // Verify that language is no longer found.
    $this->drupalGet('admin/config/regional/language/delete/' . $langcode);
147
    $this->assertResponse(404, t('Language no longer found.'));
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
    // Make sure the "language_count" variable has been updated correctly.
    drupal_static_reset('language_list');
    $enabled = language_list('enabled');
    $this->assertEqual(variable_get('language_count', 1), count($enabled[1]), t('Language count is correct.'));
    // Delete a disabled language.
    // Disable an enabled language.
    $edit = array(
      'enabled[fr]' => FALSE,
    );
    $this->drupalPost($path, $edit, t('Save configuration'));
    $this->assertNoFieldChecked('edit-enabled-fr', t('French language disabled.'));
    // Get the count of enabled languages.
    drupal_static_reset('language_list');
    $enabled = language_list('enabled');
    // Delete the disabled language.
    $this->drupalPost('admin/config/regional/language/delete/fr', array(), t('Delete'));
    // We need raw here because %locale will add HTML.
    $this->assertRaw(t('The language %locale has been removed.', array('%locale' => 'French')), t('Disabled language has been removed.'));
    $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.'));
    // Verify that language is no longer found.
    $this->drupalGet('admin/config/regional/language/delete/fr');
    $this->assertResponse(404, t('Language no longer found.'));
    // Make sure the "language_count" variable has not changed.
    $this->assertEqual(variable_get('language_count', 1), count($enabled[1]), t('Language count is correct.'));

173
174

    // Ensure we can't delete the English language.
175
    $this->drupalGet('admin/config/regional/language/delete/en');
176
177
    $this->assertEqual($this->getUrl(), url('admin/config/regional/language', array('absolute' => TRUE)), t('Correct page redirection.'));
    $this->assertText(t('The English language cannot be deleted.'), t('Failed to delete English language.'));
178
179
180
181
  }

}

182
183
184
185
/**
 * Functional test for string translation and validation.
 */
class LocaleTranslationFunctionalTest extends DrupalWebTestCase {
186
  public static function getInfo() {
187
    return array(
188
189
190
      'name' => 'String translate, search and validate',
      'description' => 'Adds a new locale and translates its name. Checks the validation of translation strings and search results.',
      'group' => 'Locale',
191
192
193
194
195
196
197
    );
  }

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

198
199
200
201
  /**
   * Adds a language and tests string translation by users with the appropriate permissions.
   */
  function testStringTranslation() {
202
203
204
205
206
207
208
    global $base_url;

    // User to add and remove language.
    $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages'));
    // User to translate and delete string.
    $translate_user = $this->drupalCreateUser(array('translate interface', 'access administration pages'));
    // Code for the language.
209
    $langcode = 'xx';
210
211
212
213
    // The English name for the language. This will be translated.
    $name = $this->randomName(16);
    // The native name for the language.
    $native = $this->randomName(16);
214
    // The domain prefix.
215
    $prefix = $langcode;
216
217
218
219
220
221
    // This is the language indicator on the translation search screen for
    // untranslated strings. Copied straight from locale.inc.
    $language_indicator = "<em class=\"locale-untranslated\">$langcode</em> ";
    // This will be the translation of $name.
    $translation = $this->randomName(16);

222
    // Add custom language.
223
    $this->drupalLogin($admin_user);
224
    $edit = array(
225
226
227
228
229
230
      'langcode' => $langcode,
      'name' => $name,
      'native' => $native,
      'prefix' => $prefix,
      'direction' => '0',
    );
231
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
232
    // Add string.
233
    t($name, array(), array('langcode' => $langcode));
234
    // Reset locale cache.
235
    locale_reset();
236
237
238
    $this->assertText($langcode, t('Language code found.'));
    $this->assertText($name, t('Name found.'));
    $this->assertText($native, t('Native found.'));
239
240
    // No t() here, we do not want to add this string to the database and it's
    // surely not translated yet.
241
    $this->assertText($native, t('Test language added.'));
242
    $this->drupalLogout();
243
244
245

    // Search for the name and translate it.
    $this->drupalLogin($translate_user);
246
    $search = array(
247
248
249
250
      'string' => $name,
      'language' => 'all',
      'translation' => 'all',
    );
251
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
252
    // assertText() seems to remove the input field where $name always could be
253
254
    // found, so this is not a false assert. See how assertNoText succeeds
    // later.
255
256
    $this->assertText($name, t('Search found the name.'));
    $this->assertRaw($language_indicator, t('Name is untranslated.'));
257
    // Assume this is the only result, given the random name.
258
259
    $this->clickLink(t('edit'));
    // We save the lid from the path.
260
    $matches = array();
261
    preg_match('!admin/config/regional/translate/edit/(\d+)!', $this->getUrl(), $matches);
262
    $lid = $matches[1];
263
    // No t() here, it's surely not translated yet.
264
    $this->assertText($name, t('name found on edit screen.'));
265
    $edit = array(
266
267
268
      "translations[$langcode]" => $translation,
    );
    $this->drupalPost(NULL, $edit, t('Save translations'));
269
270
271
    $this->assertText(t('The string has been saved.'), t('The string has been saved.'));
    $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.'));
    $this->assertTrue($name != $translation && t($name, array(), array('langcode' => $langcode)) == $translation, t('t() works.'));
272
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
273
    // The indicator should not be here.
274
    $this->assertNoRaw($language_indicator, t('String is translated.'));
275
276
277
278

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

284
    // Delete the language.
285
    $this->drupalLogin($admin_user);
286
    $path = 'admin/config/regional/language/delete/' . $langcode;
287
288
289
    // This a confirm form, we do not need any fields changed.
    $this->drupalPost($path, array(), t('Delete'));
    // We need raw here because %locale will add HTML.
290
    $this->assertRaw(t('The language %locale has been removed.', array('%locale' => $name)), t('The test language has been removed.'));
291
292
    // Reload to remove $name.
    $this->drupalGet($path);
293
294
295
    $this->assertNoText($langcode, t('Language code not found.'));
    $this->assertNoText($name, t('Name not found.'));
    $this->assertNoText($native, t('Native not found.'));
296
    $this->drupalLogout();
297

298
    // Delete the string.
299
    $this->drupalLogin($translate_user);
300
301
302
303
304
    $search = array(
      'string' => $name,
      'language' => 'all',
      'translation' => 'all',
    );
305
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
306
307
    // Assume this is the only result, given the random name.
    $this->clickLink(t('delete'));
308
    $this->assertText(t('Are you sure you want to delete the string'), t('"delete" link is correct.'));
309
    // Delete the string.
310
    $path = 'admin/config/regional/translate/delete/' . $lid;
311
312
313
    $this->drupalGet($path);
    // First test the 'cancel' link.
    $this->clickLink(t('Cancel'));
314
315
    $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.'));
    $this->assertRaw($name, t('The string was not deleted.'));
316
    // Delete the name string.
317
    $this->drupalPost('admin/config/regional/translate/delete/' . $lid, array(), t('Delete'));
318
319
    $this->assertText(t('The string has been removed.'), t('The string has been removed message.'));
    $this->assertEqual($this->getUrl(), url('admin/config/regional/translate/translate', array('absolute' => TRUE)), t('Correct page redirection.'));
320
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
321
    $this->assertNoText($name, t('Search now can not find the name.'));
322
  }
323

324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
  /*
   * Adds a language and checks that the JavaScript translation files are
   * properly created and rebuilt on deletion.
   */
  function testJavaScriptTranslation() {
    $user = $this->drupalCreateUser(array('translate interface', 'administer languages', 'access administration pages'));
    $this->drupalLogin($user);

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

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

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

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

363
    // Trigger JavaScript translation parsing and building.
364
365
    require_once DRUPAL_ROOT . '/includes/locale.inc';
    _locale_rebuild_js($langcode);
366
367
368
369
370
371
372
373
374

    // Retrieve the JavaScript translation hash code for the custom language to
    // check that the translation file has been properly built.
    $file = db_select('languages', 'l')
      ->fields('l', array('javascript'))
      ->condition('language', $langcode)
      ->execute()
      ->fetchObject();
    $js_file = 'public://' . variable_get('locale_js_directory', 'languages') . '/' . $langcode . '_' . $file->javascript . '.js';
375
    $this->assertTrue($result = file_exists($js_file), t('JavaScript file created: %file', array('%file' => $result ? $js_file : t('not found'))));
376
377
378

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

385
386
387
388
  /**
   * Tests the validation of the translation input.
   */
  function testStringValidation() {
389
390
    global $base_url;

391
    // User to add language and strings.
392
393
    $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'translate interface'));
    $this->drupalLogin($admin_user);
394
    $langcode = 'xx';
395
396
397
398
    // The English name for the language. This will be translated.
    $name = $this->randomName(16);
    // The native name for the language.
    $native = $this->randomName(16);
399
    // The domain prefix.
400
    $prefix = $langcode;
401
402
403
404
405
406
407
408
409
410
411
    // This is the language indicator on the translation search screen for
    // untranslated strings. Copied straight from locale.inc.
    $language_indicator = "<em class=\"locale-untranslated\">$langcode</em> ";
    // These will be the invalid translations of $name.
    $key = $this->randomName(16);
    $bad_translations[$key] = "<script>alert('xss');</script>" . $key;
    $key = $this->randomName(16);
    $bad_translations[$key] = '<img SRC="javascript:alert(\'xss\');">' . $key;
    $key = $this->randomName(16);
    $bad_translations[$key] = '<<SCRIPT>alert("xss");//<</SCRIPT>' . $key;
    $key = $this->randomName(16);
412
    $bad_translations[$key] ="<BODY ONLOAD=alert('xss')>" . $key;
413

414
415
    // Add custom language.
    $edit = array(
416
417
418
419
420
421
      'langcode' => $langcode,
      'name' => $name,
      'native' => $native,
      'prefix' => $prefix,
      'direction' => '0',
    );
422
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
423
    // Add string.
424
    t($name, array(), array('langcode' => $langcode));
425
    // Reset locale cache.
426
    $search = array(
427
428
429
430
      'string' => $name,
      'language' => 'all',
      'translation' => 'all',
    );
431
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
432
    // Find the edit path.
433
    $content = $this->drupalGetContent();
434
    $this->assertTrue(preg_match('@(admin/config/regional/translate/edit/[0-9]+)@', $content, $matches), t('Found the edit path.'));
435
436
    $path = $matches[0];
    foreach ($bad_translations as $key => $translation) {
437
      $edit = array(
438
439
440
441
442
        "translations[$langcode]" => $translation,
      );
      $this->drupalPost($path, $edit, t('Save translations'));
      // Check for a form error on the textarea.
      $form_class = $this->xpath('//form[@id="locale-translate-edit-form"]//textarea/@class');
443
444
      $this->assertNotIdentical(FALSE, strpos($form_class[0], 'error'), t('The string was rejected as unsafe.'));
      $this->assertNoText(t('The string has been saved.'), t('The string was not saved.'));
445
446
    }
  }
447
448
449
450
451
452
453
454
455
456
457
458
459

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

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

    // Code for the language.
460
    $langcode = 'xx';
461
462
463
464
465
    // The English name for the language. This will be translated.
    $name = $this->randomName(16);
    // The native name for the language.
    $native = $this->randomName(16);
    // The domain prefix.
466
    $prefix = $langcode;
467
468
469
    // This is the language indicator on the translation search screen for
    // untranslated strings. Copied straight from locale.inc.
    $language_indicator = "<em class=\"locale-untranslated\">$langcode</em> ";
470
471
    // This will be the translation of $name.
    $translation = $this->randomName(16);
472
473
474
475
476
477
478
479
480
481

    // Add custom language.
    $this->drupalLogin($admin_user);
    $edit = array(
      'langcode' => $langcode,
      'name' => $name,
      'native' => $native,
      'prefix' => $prefix,
      'direction' => '0',
    );
482
    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language'));
483
    // Add string.
484
    t($name, array(), array('langcode' => $langcode));
485
    // Reset locale cache.
486
    locale_reset();
487
488
489
490
491
492
493
494
495
    $this->drupalLogout();

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

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

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

    // Add translation.
    // Assume this is the only result, given the random name.
    $this->clickLink(t('edit'));
    // We save the lid from the path.
    $matches = array();
527
    preg_match('!admin/config/regional/translate/edit/(\d)+!', $this->getUrl(), $matches);
528
529
530
531
532
533
534
535
536
537
538
539
540
    $lid = $matches[1];
    $edit = array(
      "translations[$langcode]" => $translation,
    );
    $this->drupalPost(NULL, $edit, t('Save translations'));

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

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

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

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

    // Ensure translated string doesn't appear if searching on English.
    $search = array(
      'string' => $translation,
      'language' => 'en',
      'translation' => 'all',
    );
579
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
580
    $this->assertText(t('No strings available.'), t("Search didn't find the translation."));
581
582
583
584
585
586
587
588

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

/**
 * Functional tests for the import of translation files.
 */
class LocaleImportFunctionalTest extends DrupalWebTestCase {
598
  public static function getInfo() {
599
    return array(
600
      'name' => 'Translation import',
601
      'description' => 'Tests the import of locale files.',
602
      'group' => 'Locale',
603
604
605
606
607
608
609
610
611
    );
  }

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

  function setUp() {
612
    parent::setUp('locale', 'locale_test');
613
614
615
616
617
618

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

  /**
619
   * Test import of standalone .po files.
620
621
622
   */
  function testStandalonePoFile() {
    // Try importing a .po file.
623
    $this->importPoFile($this->getPoFile(), array(
624
      'langcode' => 'fr',
625
    ));
626

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

    // The import should have created 7 strings.
631
    $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)), t('The translation file was successfully imported.'));
632

633
    // This import should have saved plural forms to have 2 variants.
634
    $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, t('Plural number initialized.'));
635

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

639

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

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

650

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

660

661
662
    // Try importing a .po file with overriding strings, and ensure existing
    // strings are kept.
663
    $this->importPoFile($this->getOverwritePoFile(), array(
664
665
      'langcode' => 'fr',
      'mode' => 1, // Existing strings are kept, only new strings are added.
666
667
    ));

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

679
    // This import should not have changed number of plural forms.
680
    $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 2, t('Plural numbers untouched.'));
681

682
683
    // Try importing a .po file with overriding strings, and ensure existing
    // strings are overwritten.
684
    $this->importPoFile($this->getOverwritePoFile(), array(
685
686
      'langcode' => 'fr',
      'mode' => 0, // Strings in the uploaded file replace existing ones, new ones are added.
687
688
    ));

689
    // The import should have updated 2 strings.
690
    $this->assertRaw(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => 0, '%update' => 2, '%delete' => 0)), t('The translation file was successfully imported.'));
691
692
693
694
695
696
    // Ensure string was overwritten.
    $search = array(
      'string' => 'Montag',
      'language' => 'fr',
      'translation' => 'translated',
    );
697
    $this->drupalPost('admin/config/regional/translate/translate', $search, t('Filter'));
698
    $this->assertNoText(t('No strings available.'), t('String overwritten by imported string.'));
699
    // This import should have changed number of plural forms.
700
    $this->assert(db_query("SELECT plurals FROM {languages} WHERE language = 'fr'")->fetchField() == 3, t('Plural numbers changed.'));
701
702
703
  }

  /**
704
705
   * Test automatic import of a module's translation files when a language is
   * enabled.
706
707
   */
  function testAutomaticModuleTranslationImportLanguageEnable() {
708
709
    // Code for the language - manually set to match the test translation file.
    $langcode = 'xx';
710
711
712
713
714
    // The English name for the language.
    $name = $this->randomName(16);
    // The native name for the language.
    $native = $this->randomName(16);
    // The domain prefix.
715
    $prefix = $langcode;
716
717
718
719
720
721
722
723
724

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

    // Ensure the translation file was automatically imported when language was
    // added.
729
    $this->assertText(t('One translation file imported for the enabled modules.'), t('Language file automatically imported.'));
730
731
732
733
734
735
736

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

741
  /**
742
   * Test msgctxt context support.
743
744
745
746
747
748
749
   */
  function testLanguageContext() {
    // Try importing a .po file.
    $this->importPoFile($this->getPoFileWithContext(), array(
      'langcode' => 'hr',
    ));

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

754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
  /**
   * Test empty msgstr at end of .po file see #611786.
   */
  function testEmptyMsgstr() {
    $langcode = 'hu';

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

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

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

790
791
792
793
794
795
796
797
798
  /**
   * Helper function: import a standalone .po file in a given language.
   *
   * @param $contents
   *   Contents of the .po file to import.
   * @param $options
   *   Additional options to pass to the translation import form.
   */
  function importPoFile($contents, array $options = array()) {
799
    $name = tempnam('temporary://', "po_") . '.po';
800
801
    file_put_contents($name, $contents);
    $options['files[file]'] = $name;
802
    $this->drupalPost('admin/config/regional/translate/import', $options, t('Import'));
803
    drupal_unlink($name);
804
805
  }

806
807
808
809
810
811
812
  /**
   * Helper function that returns a proper .po file.
   */
  function getPoFile() {
    return <<< EOF
msgid ""
msgstr ""
813
"Project-Id-Version: Drupal 7\\n"
814
815
816
817
818
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\\n"

819
820
821
822
823
msgid "One sheep"
msgid_plural "@count sheep"
msgstr[0] "un mouton"
msgstr[1] "@count moutons"

824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
msgid "Monday"
msgstr "lundi"

msgid "Tuesday"
msgstr "mardi"

msgid "Wednesday"
msgstr "mercredi"

msgid "Thursday"
msgstr "jeudi"

msgid "Friday"
msgstr "vendredi"

msgid "Saturday"
msgstr "samedi"

msgid "Sunday"
msgstr "dimanche"
844
845
846
847
EOF;
  }

  /**
848
   * Helper function that returns a bad .po file.
849
850
851
852
853
   */
  function getBadPoFile() {
    return <<< EOF
msgid ""
msgstr ""
854
"Project-Id-Version: Drupal 7\\n"
855
856
857
858
859
860
861
862
863
864
865
866
867
868
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\\n"

msgid "Save configuration"
msgstr "Enregistrer la configuration"

msgid "edit"
msgstr "modifier<img SRC="javascript:alert(\'xss\');">"

msgid "delete"
msgstr "supprimer<script>alert('xss');</script>"

869
870
EOF;
  }
871
872
873
874
875
876
877
878
879

  /**
   * Helper function that returns a proper .po file, for testing overwriting
   * existing translations.
   */
  function getOverwritePoFile() {
    return <<< EOF
msgid ""
msgstr ""
880
"Project-Id-Version: Drupal 7\\n"
881
882
883
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
884
"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"
885
886
887
888
889
890
891
892
893

msgid "Monday"
msgstr "Montag"

msgid "Day"
msgstr "Jour"
EOF;
  }

894
895
896
897
898
899
900
901
902
  /**
   * Helper function that returns a .po file with context.
   */
  function getPoFileWithContext() {
    // Croatian (code hr) is one the the languages that have a different
    // form for the full name and the abbreviated name for the month May.
    return <<< EOF
msgid ""
msgstr ""
903
"Project-Id-Version: Drupal 7\\n"
904
905
906
907
908
909
910
911
912
913
914
915
916
917
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n"

msgctxt "Long month name"
msgid "May"
msgstr "Svibanj"

msgid "May"
msgstr "Svi."
EOF;
  }

918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
  /**
   * Helper function that returns a .po file with an empty last item.
   */
  function getPoFileWithEmptyMsgstr() {
    return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 7\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\\n"

msgid "Operations"
msgstr ""

EOF;
  }
  /**
   * Helper function that returns a .po file with an empty last item.
   */
  function getPoFileWithMsgstr() {
    return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 7\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\\n"

msgid "Operations"
msgstr "Műveletek"

msgid "Will not appear in Drupal core, so we can ensure the test passes"
msgstr ""

EOF;
  }
957

958
959
960
961
962
963
}

/**
 * Functional tests for the export of translation files.
 */
class LocaleExportFunctionalTest extends DrupalWebTestCase {
964
  public static function getInfo() {
965
    return array(
966
967
968
      'name' => 'Translation export',
      'description' => 'Tests the exportation of locale files.',
      'group' => 'Locale',
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
    );
  }

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

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

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

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

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

    // Ensure we have a translation file.
1004
    $this->assertRaw('# French translation of Drupal', t('Exported French translation file.'));
1005
    // Ensure our imported translations exist in the file.
1006
    $this->assertRaw('msgstr "lundi"', t('French translations present in exported file.'));
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
  }

  /**
   * Test exportation of translation template file.
   */
  function testExportTranslationTemplateFile() {
    // Get the translation template file.
    // There are two 'Export' buttons on this page, but it somehow works.  It'd
    // be better if we could use the submit button id like documented but that
    // doesn't work.
1017
    $this->drupalPost('admin/config/regional/translate/export', array(), t('Export'));
1018
    // Ensure we have a translation file.
1019
    $this->assertRaw('# LANGUAGE translation of PROJECT', t('Exported translation template file.'));
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
  }

  /**
   * Helper function that returns a proper .po file.
   */
  function getPoFile() {
    return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 6\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\\n"

msgid "Monday"
msgstr "lundi"
EOF;
  }

1040
}
1041

1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
/**
 * Tests for the st() function.
 */
class LocaleInstallTest extends DrupalWebTestCase {
  public static function getInfo() {
    return array(
      'name' => 'String translation using st()',
      'description' => 'Tests that st() works like t().',
      'group' => 'Locale',
    );
  }

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

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

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

1071
1072
1073
1074
/**
 * Locale uninstall with English UI functional test.
 */
class LocaleUninstallFunctionalTest extends DrupalWebTestCase {
1075
  public static function getInfo() {
1076
    return array(
1077
1078
1079
      'name' => 'Locale uninstall (EN)',
      'description' => 'Tests the uninstall process using the built-in UI language.',
      'group' => 'Locale',
1080
1081
1082
1083
1084
1085
    );
  }

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

1088
1089
  function setUp() {
    parent::setUp('locale');
1090
    $this->language = 'en';
1091
  }
1092

1093
1094
1095
1096
1097
  /**
   * Check if the values of the Locale variables are correct after uninstall.
   */
  function testUninstallProcess() {
    $locale_module = array('locale');
1098

1099
1100
    // Add a new language and optionally set it as default.
    require_once DRUPAL_ROOT . '/includes/locale.inc';
1101
    locale_add_language('fr', 'French', 'Français', LANGUAGE_LTR, '', '', TRUE, $this->language == 'fr');
1102

1103
    // Check the UI language.
1104
    drupal_language_initialize();
1105
    global $language;
1106
    $this->assertEqual($language->language, $this->language, t('Current language: %lang', array('%lang' => $language->language)));
1107

1108
1109
    // Enable multilingual workflow option for articles.
    variable_set('language_content_type_article', 1);
1110

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

1114
1115
1116
    // Build the JavaScript translation file for French.
    $user = $this->drupalCreateUser(array('translate interface', 'access administration pages'));
    $this->drupalLogin($user);
1117
    $this->drupalGet('admin/config/regional/translate/translate');
1118
    $string = db_query('SELECT min(lid) AS lid FROM {locales_source} WHERE location LIKE :location', array(
1119
1120
      ':location' => '%.js%',
    ))->fetchObject();