LocalePluralFormatTest.php 16.2 KB
Newer Older
1 2
<?php

3
namespace Drupal\Tests\locale\Functional;
4

5
use Drupal\Component\Gettext\PoItem;
6
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
7
use Drupal\Tests\BrowserTestBase;
8 9

/**
10 11 12
 * Tests plural handling for various languages.
 *
 * @group locale
13
 */
14
class LocalePluralFormatTest extends BrowserTestBase {
15

catch's avatar
catch committed
16 17 18 19 20 21 22
  /**
   * An admin user.
   *
   * @var \Drupal\user\Entity\User
   */
  protected $adminUser;

23 24 25 26 27
  /**
   * Modules to enable.
   *
   * @var array
   */
28
  public static $modules = ['locale'];
29

30 31 32
  /**
   * {@inheritdoc}
   */
33
  protected function setUp() {
34
    parent::setUp();
35

36
    $this->adminUser = $this->drupalCreateUser(['administer languages', 'translate interface', 'access administration pages']);
catch's avatar
catch committed
37
    $this->drupalLogin($this->adminUser);
38 39 40
  }

  /**
41 42
   * Tests locale_get_plural() and \Drupal::translation()->formatPlural()
   * functionality.
43
   */
44
  public function testGetPluralFormat() {
45
    // Import some .po files with formulas to set up the environment.
46
    // These will also add the languages to the system.
47
    $this->importPoFile($this->getPoFileWithSimplePlural(), [
48
      'langcode' => 'fr',
49 50
    ]);
    $this->importPoFile($this->getPoFileWithComplexPlural(), [
51
      'langcode' => 'hr',
52
    ]);
53 54 55

    // Attempt to import some broken .po files as well to prove that these
    // will not overwrite the proper plural formula imported above.
56
    $this->importPoFile($this->getPoFileWithMissingPlural(), [
57 58
      'langcode' => 'fr',
      'overwrite_options[not_customized]' => TRUE,
59 60
    ]);
    $this->importPoFile($this->getPoFileWithBrokenPlural(), [
61 62
      'langcode' => 'hr',
      'overwrite_options[not_customized]' => TRUE,
63
    ]);
64 65 66 67 68 69 70

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

    // Expected plural translation strings for each plural index.
71
    $plural_strings = [
72 73
      // English is not imported in this case, so we assume built-in text
      // and formulas.
74
      'en' => [
75 76
        0 => '1 hour',
        1 => '@count hours',
77 78
      ],
      'fr' => [
79
        0 => '@count heure',
80
        1 => '@count heures',
81 82
      ],
      'hr' => [
83 84 85
        0 => '@count sat',
        1 => '@count sata',
        2 => '@count sati',
86
      ],
87 88 89
      // Hungarian is not imported, so it should assume the same text as
      // English, but it will always pick the plural form as per the built-in
      // logic, so only index -1 is relevant with the plural value.
90
      'hu' => [
91 92
        0 => '1 hour',
        -1 => '@count hours',
93 94
      ],
    ];
95 96 97

    // Expected plural indexes precomputed base on the plural formulas with
    // given $count value.
98 99
    $plural_tests = [
      'en' => [
100 101 102
        1 => 0,
        0 => 1,
        5 => 1,
103 104
        123 => 1,
        235 => 1,
105 106
      ],
      'fr' => [
107 108 109
        1 => 0,
        0 => 0,
        5 => 1,
110 111
        123 => 1,
        235 => 1,
112 113
      ],
      'hr' => [
114 115 116 117 118
        1 => 0,
        21 => 0,
        0 => 2,
        2 => 1,
        8 => 2,
119 120
        123 => 1,
        235 => 2,
121 122
      ],
      'hu' => [
123 124 125
        1 => -1,
        21 => -1,
        0 => -1,
126 127
      ],
    ];
128 129 130 131 132 133 134 135 136

    foreach ($plural_tests as $langcode => $tests) {
      foreach ($tests as $count => $expected_plural_index) {
        // Assert that the we get the right plural index.
        $this->assertIdentical(locale_get_plural($count, $langcode), $expected_plural_index, 'Computed plural index for ' . $langcode . ' for count ' . $count . ' is ' . $expected_plural_index);
        // Assert that the we get the right translation for that. Change the
        // expected index as per the logic for translation lookups.
        $expected_plural_index = ($count == 1) ? 0 : $expected_plural_index;
        $expected_plural_string = str_replace('@count', $count, $plural_strings[$langcode][$expected_plural_index]);
137
        $this->assertIdentical(\Drupal::translation()->formatPlural($count, '1 hour', '@count hours', [], ['langcode' => $langcode])->render(), $expected_plural_string, 'Plural translation of 1 hours / @count hours for count ' . $count . ' in ' . $langcode . ' is ' . $expected_plural_string);
138
        // DO NOT use translation to pass translated strings into
139
        // PluralTranslatableMarkup::createFromTranslatedString() this way. It
140 141 142
        // is designed to be used with *already* translated text like settings
        // from configuration. We use PHP translation here just because we have
        // the expected result data in that format.
143
        $translated_string = \Drupal::translation()->translate('1 hour' . PoItem::DELIMITER . '@count hours', [], ['langcode' => $langcode]);
144
        $plural = PluralTranslatableMarkup::createFromTranslatedString($count, $translated_string, [], ['langcode' => $langcode]);
145
        $this->assertIdentical($plural->render(), $expected_plural_string);
146 147 148 149
      }
    }
  }

150 151 152 153 154 155 156
  /**
   * Tests plural editing of DateFormatter strings
   */
  public function testPluralEditDateFormatter() {

    // Import some .po files with formulas to set up the environment.
    // These will also add the languages to the system.
157
    $this->importPoFile($this->getPoFileWithSimplePlural(), [
158
      'langcode' => 'fr',
159
    ]);
160 161 162 163

    // Set French as the site default language.
    $this->config('system.site')->set('default_langcode', 'fr')->save();

catch's avatar
catch committed
164 165 166 167
    // Visit User Info page before updating translation strings. Change the
    // created time to ensure that the we're dealing in seconds and it can't be
    // exactly 1 minute.
    $this->adminUser->set('created', time() - 1)->save();
168 169 170 171 172 173
    $this->drupalGet('user');

    // Member for time should be translated.
    $this->assertText("seconde", "'Member for' text is translated.");

    $path = 'admin/config/regional/translate/';
174
    $search = [
175
      'langcode' => 'fr',
176 177 178
      // Limit to only translated strings to ensure that database ordering does
      // not break the test.
      'translation' => 'translated',
179
    ];
180 181 182 183 184 185 186 187 188
    $this->drupalPostForm($path, $search, t('Filter'));
    // Plural values for the langcode fr.
    $this->assertText('@count seconde');
    $this->assertText('@count secondes');

    // Inject a plural source string to the database. We need to use a specific
    // langcode here because the language will be English by default and will
    // not save our source string for performance optimization if we do not ask
    // specifically for a language.
189
    \Drupal::translation()->formatPlural(1, '1 second', '@count seconds', [], ['langcode' => 'fr'])->render();
190
    $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = ''", [':source' => "1 second" . PoItem::DELIMITER . "@count seconds"])->fetchField();
191
    // Look up editing page for this plural string and check fields.
192
    $search = [
193 194
      'string' => '1 second',
      'langcode' => 'fr',
195
    ];
196 197 198
    $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));

    // Save complete translations for the string in langcode fr.
199
    $edit = [
200 201
      "strings[$lid][translations][0]" => '1 seconde updated',
      "strings[$lid][translations][1]" => '@count secondes updated',
202
    ];
203 204 205 206 207
    $this->drupalPostForm($path, $edit, t('Save translations'));

    // User interface input for translating seconds should not be duplicated
    $this->assertUniqueText('@count seconds', 'Interface translation input for @count seconds only appears once.');

catch's avatar
catch committed
208 209 210 211
    // Member for time should be translated. Change the created time to ensure
    // that the we're dealing in multiple seconds and it can't be exactly 1
    // second or minute.
    $this->adminUser->set('created', time() - 2)->save();
212
    $this->drupalGet('user');
catch's avatar
catch committed
213
    $this->assertText("secondes updated", "'Member for' text is translated.");
214 215
  }

216 217 218
  /**
   * Tests plural editing and export functionality.
   */
219
  public function testPluralEditExport() {
220
    // Import some .po files with formulas to set up the environment.
221
    // These will also add the languages to the system.
222
    $this->importPoFile($this->getPoFileWithSimplePlural(), [
223
      'langcode' => 'fr',
224 225
    ]);
    $this->importPoFile($this->getPoFileWithComplexPlural(), [
226
      'langcode' => 'hr',
227
    ]);
228 229

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

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

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

    // Look up editing page for this plural string and check fields.
257
    $path = 'admin/config/regional/translate/';
258
    $search = [
259
      'langcode' => 'hr',
260
    ];
261
    $this->drupalPostForm($path, $search, t('Filter'));
262
    // Labels for plural editing elements.
263 264 265 266 267 268 269 270 271 272 273
    $this->assertText('Singular form');
    $this->assertText('First plural form');
    $this->assertText('2. plural form');
    $this->assertNoText('3. plural form');

    // Plural values for langcode hr.
    $this->assertText('@count sat');
    $this->assertText('@count sata');
    $this->assertText('@count sati');

    // Edit langcode hr translations and see if that took effect.
274
    $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = ''", [':source' => "1 hour" . PoItem::DELIMITER . "@count hours"])->fetchField();
275
    $edit = [
276
      "strings[$lid][translations][1]" => '@count sata edited',
277
    ];
278
    $this->drupalPostForm($path, $edit, t('Save translations'));
279

280
    $search = [
281
      'langcode' => 'fr',
282
    ];
283
    $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
284
    // Plural values for the langcode fr.
285
    $this->assertText('@count heure');
286 287 288 289
    $this->assertText('@count heures');
    $this->assertNoText('2. plural form');

    // Edit langcode fr translations and see if that took effect.
290
    $edit = [
291
      "strings[$lid][translations][0]" => '@count heure edited',
292
    ];
293
    $this->drupalPostForm($path, $edit, t('Save translations'));
294 295 296 297 298

    // Inject a plural source string to the database. We need to use a specific
    // langcode here because the language will be English by default and will
    // not save our source string for performance optimization if we do not ask
    // specifically for a language.
299
    \Drupal::translation()->formatPlural(1, '1 day', '@count days', [], ['langcode' => 'fr'])->render();
300
    $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = ''", [':source' => "1 day" . PoItem::DELIMITER . "@count days"])->fetchField();
301
    // Look up editing page for this plural string and check fields.
302
    $search = [
303 304
      'string' => '1 day',
      'langcode' => 'fr',
305
    ];
306
    $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
307

308
    // Save complete translations for the string in langcode fr.
309
    $edit = [
310 311
      "strings[$lid][translations][0]" => '1 jour',
      "strings[$lid][translations][1]" => '@count jours',
312
    ];
313
    $this->drupalPostForm($path, $edit, t('Save translations'));
314

315
    // Save complete translations for the string in langcode hr.
316
    $search = [
317 318
      'string' => '1 day',
      'langcode' => 'hr',
319
    ];
320
    $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
321

322
    $edit = [
323 324 325
      "strings[$lid][translations][0]" => '@count dan',
      "strings[$lid][translations][1]" => '@count dana',
      "strings[$lid][translations][2]" => '@count dana',
326
    ];
327
    $this->drupalPostForm($path, $edit, t('Save translations'));
328

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

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

  /**
   * Imports a standalone .po file in a given language.
   *
349
   * @param string $contents
350
   *   Contents of the .po file to import.
351
   * @param array $options
352 353
   *   Additional options to pass to the translation import form.
   */
354
  public function importPoFile($contents, array $options = []) {
355 356
    $file_system = \Drupal::service('file_system');
    $name = $file_system->tempnam('temporary://', "po_") . '.po';
357 358
    file_put_contents($name, $contents);
    $options['files[file]'] = $name;
359
    $this->drupalPostForm('admin/config/regional/translate/import', $options, t('Import'));
360
    $file_system->unlink($name);
361 362 363 364 365
  }

  /**
   * Returns a .po file with a simple plural formula.
   */
366
  public function getPoFileWithSimplePlural() {
367 368 369 370 371 372 373 374 375 376 377
    return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\\n"

msgid "1 hour"
msgid_plural "@count hours"
378
msgstr[0] "@count heure"
379 380
msgstr[1] "@count heures"

381 382 383 384 385
msgid "1 second"
msgid_plural "@count seconds"
msgstr[0] "@count seconde"
msgstr[1] "@count secondes"

386 387 388 389 390 391 392 393
msgid "Monday"
msgstr "lundi"
EOF;
  }

  /**
   * Returns a .po file with a complex plural formula.
   */
394
  public function getPoFileWithComplexPlural() {
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
    return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n"

msgid "1 hour"
msgid_plural "@count hours"
msgstr[0] "@count sat"
msgstr[1] "@count sata"
msgstr[2] "@count sati"

msgid "Monday"
msgstr "Ponedjeljak"
EOF;
  }

  /**
   * Returns a .po file with a missing plural formula.
   */
418
  public function getPoFileWithMissingPlural() {
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434
    return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"

msgid "Monday"
msgstr "lundi"
EOF;
  }

  /**
   * Returns a .po file with a broken plural formula.
   */
435
  public function getPoFileWithBrokenPlural() {
436 437 438 439 440 441 442 443 444 445 446 447 448
    return <<< EOF
msgid ""
msgstr ""
"Project-Id-Version: Drupal 8\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: broken, will not parse\\n"

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

450
}