3 namespace Drupal\Tests\locale\Functional;
5 use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
6 use Drupal\Tests\BrowserTestBase;
9 * Tests plural handling for various languages.
13 class LocalePluralFormatTest extends BrowserTestBase {
18 * @var \Drupal\user\Entity\User
27 public static $modules = ['locale'];
32 protected function setUp() {
35 $this->adminUser = $this->drupalCreateUser(['administer languages', 'translate interface', 'access administration pages']);
36 $this->drupalLogin($this->adminUser);
40 * Tests locale_get_plural() and \Drupal::translation()->formatPlural()
43 public function testGetPluralFormat() {
44 // Import some .po files with formulas to set up the environment.
45 // These will also add the languages to the system.
46 $this->importPoFile($this->getPoFileWithSimplePlural(), [
49 $this->importPoFile($this->getPoFileWithComplexPlural(), [
53 // Attempt to import some broken .po files as well to prove that these
54 // will not overwrite the proper plural formula imported above.
55 $this->importPoFile($this->getPoFileWithMissingPlural(), [
57 'overwrite_options[not_customized]' => TRUE,
59 $this->importPoFile($this->getPoFileWithBrokenPlural(), [
61 'overwrite_options[not_customized]' => TRUE,
64 // Reset static caches from locale_get_plural() to ensure we get fresh data.
65 drupal_static_reset('locale_get_plural');
66 drupal_static_reset('locale_get_plural:plurals');
67 drupal_static_reset('locale');
69 // Expected plural translation strings for each plural index.
71 // English is not imported in this case, so we assume built-in text
86 // Hungarian is not imported, so it should assume the same text as
87 // English, but it will always pick the plural form as per the built-in
88 // logic, so only index -1 is relevant with the plural value.
95 // Expected plural indexes precomputed base on the plural formulas with
96 // given $count value.
128 foreach ($plural_tests as $langcode => $tests) {
129 foreach ($tests as $count => $expected_plural_index) {
130 // Assert that the we get the right plural index.
131 $this->assertIdentical(locale_get_plural($count, $langcode), $expected_plural_index, 'Computed plural index for ' . $langcode . ' for count ' . $count . ' is ' . $expected_plural_index);
132 // Assert that the we get the right translation for that. Change the
133 // expected index as per the logic for translation lookups.
134 $expected_plural_index = ($count == 1) ? 0 : $expected_plural_index;
135 $expected_plural_string = str_replace('@count', $count, $plural_strings[$langcode][$expected_plural_index]);
136 $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);
137 // DO NOT use translation to pass translated strings into
138 // PluralTranslatableMarkup::createFromTranslatedString() this way. It
139 // is designed to be used with *already* translated text like settings
140 // from configuration. We use PHP translation here just because we have
141 // the expected result data in that format.
142 $translated_string = \Drupal::translation()->translate('1 hour' . PluralTranslatableMarkup::DELIMITER . '@count hours', [], ['langcode' => $langcode]);
143 $plural = PluralTranslatableMarkup::createFromTranslatedString($count, $translated_string, [], ['langcode' => $langcode]);
144 $this->assertIdentical($plural->render(), $expected_plural_string);
150 * Tests plural editing of DateFormatter strings
152 public function testPluralEditDateFormatter() {
154 // Import some .po files with formulas to set up the environment.
155 // These will also add the languages to the system.
156 $this->importPoFile($this->getPoFileWithSimplePlural(), [
160 // Set French as the site default language.
161 $this->config('system.site')->set('default_langcode', 'fr')->save();
163 // Visit User Info page before updating translation strings. Change the
164 // created time to ensure that the we're dealing in seconds and it can't be
166 $this->adminUser->set('created', time() - 1)->save();
167 $this->drupalGet('user');
169 // Member for time should be translated.
170 $this->assertText("seconde", "'Member for' text is translated.");
172 $path = 'admin/config/regional/translate/';
175 // Limit to only translated strings to ensure that database ordering does
176 // not break the test.
177 'translation' => 'translated',
179 $this->drupalPostForm($path, $search, t('Filter'));
180 // Plural values for the langcode fr.
181 $this->assertText('@count seconde');
182 $this->assertText('@count secondes');
184 // Inject a plural source string to the database. We need to use a specific
185 // langcode here because the language will be English by default and will
186 // not save our source string for performance optimization if we do not ask
187 // specifically for a language.
188 \Drupal::translation()->formatPlural(1, '1 second', '@count seconds', [], ['langcode' => 'fr'])->render();
189 $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = ''", [':source' => "1 second" . LOCALE_PLURAL_DELIMITER . "@count seconds"])->fetchField();
190 // Look up editing page for this plural string and check fields.
192 'string' => '1 second',
195 $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
197 // Save complete translations for the string in langcode fr.
199 "strings[$lid][translations][0]" => '1 seconde updated',
200 "strings[$lid][translations][1]" => '@count secondes updated',
202 $this->drupalPostForm($path, $edit, t('Save translations'));
204 // User interface input for translating seconds should not be duplicated
205 $this->assertUniqueText('@count seconds', 'Interface translation input for @count seconds only appears once.');
207 // Member for time should be translated. Change the created time to ensure
208 // that the we're dealing in multiple seconds and it can't be exactly 1
210 $this->adminUser->set('created', time() - 2)->save();
211 $this->drupalGet('user');
212 $this->assertText("secondes updated", "'Member for' text is translated.");
216 * Tests plural editing and export functionality.
218 public function testPluralEditExport() {
219 // Import some .po files with formulas to set up the environment.
220 // These will also add the languages to the system.
221 $this->importPoFile($this->getPoFileWithSimplePlural(), [
224 $this->importPoFile($this->getPoFileWithComplexPlural(), [
228 // Get the French translations.
229 $this->drupalPostForm('admin/config/regional/translate/export', [
232 // Ensure we have a translation file.
233 $this->assertRaw('# French translation of Drupal', 'Exported French translation file.');
234 // Ensure our imported translations exist in the file.
235 $this->assertRaw("msgid \"Monday\"\nmsgstr \"lundi\"", 'French translations present in exported file.');
236 // Check for plural export specifically.
237 $this->assertRaw("msgid \"1 hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"@count heure\"\nmsgstr[1] \"@count heures\"", 'Plural translations exported properly.');
239 // Get the Croatian translations.
240 $this->drupalPostForm('admin/config/regional/translate/export', [
243 // Ensure we have a translation file.
244 $this->assertRaw('# Croatian translation of Drupal', 'Exported Croatian translation file.');
245 // Ensure our imported translations exist in the file.
246 $this->assertRaw("msgid \"Monday\"\nmsgstr \"Ponedjeljak\"", 'Croatian translations present in exported file.');
247 // Check for plural export specifically.
248 $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 // Check if the source appears on the translation page.
251 $this->drupalGet('admin/config/regional/translate');
252 $this->assertText("1 hour");
253 $this->assertText("@count hours");
255 // Look up editing page for this plural string and check fields.
256 $path = 'admin/config/regional/translate/';
260 $this->drupalPostForm($path, $search, t('Filter'));
261 // Labels for plural editing elements.
262 $this->assertText('Singular form');
263 $this->assertText('First plural form');
264 $this->assertText('2. plural form');
265 $this->assertNoText('3. plural form');
267 // Plural values for langcode hr.
268 $this->assertText('@count sat');
269 $this->assertText('@count sata');
270 $this->assertText('@count sati');
272 // Edit langcode hr translations and see if that took effect.
273 $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = ''", [':source' => "1 hour" . LOCALE_PLURAL_DELIMITER . "@count hours"])->fetchField();
275 "strings[$lid][translations][1]" => '@count sata edited',
277 $this->drupalPostForm($path, $edit, t('Save translations'));
282 $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
283 // Plural values for the langcode fr.
284 $this->assertText('@count heure');
285 $this->assertText('@count heures');
286 $this->assertNoText('2. plural form');
288 // Edit langcode fr translations and see if that took effect.
290 "strings[$lid][translations][0]" => '@count heure edited',
292 $this->drupalPostForm($path, $edit, t('Save translations'));
294 // Inject a plural source string to the database. We need to use a specific
295 // langcode here because the language will be English by default and will
296 // not save our source string for performance optimization if we do not ask
297 // specifically for a language.
298 \Drupal::translation()->formatPlural(1, '1 day', '@count days', [], ['langcode' => 'fr'])->render();
299 $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = ''", [':source' => "1 day" . LOCALE_PLURAL_DELIMITER . "@count days"])->fetchField();
300 // Look up editing page for this plural string and check fields.
305 $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
307 // Save complete translations for the string in langcode fr.
309 "strings[$lid][translations][0]" => '1 jour',
310 "strings[$lid][translations][1]" => '@count jours',
312 $this->drupalPostForm($path, $edit, t('Save translations'));
314 // Save complete translations for the string in langcode hr.
319 $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
322 "strings[$lid][translations][0]" => '@count dan',
323 "strings[$lid][translations][1]" => '@count dana',
324 "strings[$lid][translations][2]" => '@count dana',
326 $this->drupalPostForm($path, $edit, t('Save translations'));
328 // Get the French translations.
329 $this->drupalPostForm('admin/config/regional/translate/export', [
332 // Check for plural export specifically.
333 $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.');
334 $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 // Get the Croatian translations.
337 $this->drupalPostForm('admin/config/regional/translate/export', [
340 // Check for plural export specifically.
341 $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.');
342 $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.');
346 * Imports a standalone .po file in a given language.
348 * @param string $contents
349 * Contents of the .po file to import.
350 * @param array $options
351 * Additional options to pass to the translation import form.
353 public function importPoFile($contents, array $options = []) {
354 $name = \Drupal::service('file_system')->tempnam('temporary://', "po_") . '.po';
355 file_put_contents($name, $contents);
356 $options['files[file]'] = $name;
357 $this->drupalPostForm('admin/config/regional/translate/import', $options, t('Import'));
358 drupal_unlink($name);
362 * Returns a .po file with a simple plural formula.
364 public function getPoFileWithSimplePlural() {
368 "Project-Id-Version: Drupal 8\\n"
369 "MIME-Version: 1.0\\n"
370 "Content-Type: text/plain; charset=UTF-8\\n"
371 "Content-Transfer-Encoding: 8bit\\n"
372 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
375 msgid_plural "@count hours"
376 msgstr[0] "@count heure"
377 msgstr[1] "@count heures"
380 msgid_plural "@count seconds"
381 msgstr[0] "@count seconde"
382 msgstr[1] "@count secondes"
390 * Returns a .po file with a complex plural formula.
392 public function getPoFileWithComplexPlural() {
396 "Project-Id-Version: Drupal 8\\n"
397 "MIME-Version: 1.0\\n"
398 "Content-Type: text/plain; charset=UTF-8\\n"
399 "Content-Transfer-Encoding: 8bit\\n"
400 "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"
403 msgid_plural "@count hours"
404 msgstr[0] "@count sat"
405 msgstr[1] "@count sata"
406 msgstr[2] "@count sati"
414 * Returns a .po file with a missing plural formula.
416 public function getPoFileWithMissingPlural() {
420 "Project-Id-Version: Drupal 8\\n"
421 "MIME-Version: 1.0\\n"
422 "Content-Type: text/plain; charset=UTF-8\\n"
423 "Content-Transfer-Encoding: 8bit\\n"
431 * Returns a .po file with a broken plural formula.
433 public function getPoFileWithBrokenPlural() {
437 "Project-Id-Version: Drupal 8\\n"
438 "MIME-Version: 1.0\\n"
439 "Content-Type: text/plain; charset=UTF-8\\n"
440 "Content-Transfer-Encoding: 8bit\\n"
441 "Plural-Forms: broken, will not parse\\n"