2315dbd22b4d9f63cc560a42e47567b00e11d6d2
[yaffs-website] / web / core / modules / locale / src / Tests / LocalePluralFormatTest.php
1 <?php
2
3 namespace Drupal\locale\Tests;
4
5 use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
6 use Drupal\simpletest\WebTestBase;
7
8 /**
9  * Tests plural handling for various languages.
10  *
11  * @group locale
12  */
13 class LocalePluralFormatTest extends WebTestBase {
14
15   /**
16    * An admin user.
17    *
18    * @var \Drupal\user\Entity\User
19    */
20   protected $adminUser;
21
22   /**
23    * Modules to enable.
24    *
25    * @var array
26    */
27   public static $modules = ['locale'];
28
29   /**
30    * {@inheritdoc}
31    */
32   protected function setUp() {
33     parent::setUp();
34
35     $this->adminUser = $this->drupalCreateUser(['administer languages', 'translate interface', 'access administration pages']);
36     $this->drupalLogin($this->adminUser);
37   }
38
39   /**
40    * Tests locale_get_plural() and \Drupal::translation()->formatPlural()
41    * functionality.
42    */
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(), [
47       'langcode' => 'fr',
48     ]);
49     $this->importPoFile($this->getPoFileWithComplexPlural(), [
50       'langcode' => 'hr',
51     ]);
52
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(), [
56       'langcode' => 'fr',
57       'overwrite_options[not_customized]' => TRUE,
58     ]);
59     $this->importPoFile($this->getPoFileWithBrokenPlural(), [
60       'langcode' => 'hr',
61       'overwrite_options[not_customized]' => TRUE,
62     ]);
63
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');
68
69     // Expected plural translation strings for each plural index.
70     $plural_strings = [
71       // English is not imported in this case, so we assume built-in text
72       // and formulas.
73       'en' => [
74         0 => '1 hour',
75         1 => '@count hours',
76       ],
77       'fr' => [
78         0 => '@count heure',
79         1 => '@count heures',
80       ],
81       'hr' => [
82         0 => '@count sat',
83         1 => '@count sata',
84         2 => '@count sati',
85       ],
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.
89       'hu' => [
90         0 => '1 hour',
91         -1 => '@count hours',
92       ],
93     ];
94
95     // Expected plural indexes precomputed base on the plural formulas with
96     // given $count value.
97     $plural_tests = [
98       'en' => [
99         1 => 0,
100         0 => 1,
101         5 => 1,
102         123 => 1,
103         235 => 1,
104       ],
105       'fr' => [
106         1 => 0,
107         0 => 0,
108         5 => 1,
109         123 => 1,
110         235 => 1,
111       ],
112       'hr' => [
113         1 => 0,
114         21 => 0,
115         0 => 2,
116         2 => 1,
117         8 => 2,
118         123 => 1,
119         235 => 2,
120       ],
121       'hu' => [
122         1 => -1,
123         21 => -1,
124         0 => -1,
125       ],
126     ];
127
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);
145       }
146     }
147   }
148
149   /**
150    * Tests plural editing of DateFormatter strings
151    */
152   public function testPluralEditDateFormatter() {
153
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(), [
157       'langcode' => 'fr',
158     ]);
159
160     // Set French as the site default language.
161     $this->config('system.site')->set('default_langcode', 'fr')->save();
162
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
165     // exactly 1 minute.
166     $this->adminUser->set('created', time() - 1)->save();
167     $this->drupalGet('user');
168
169     // Member for time should be translated.
170     $this->assertText("seconde", "'Member for' text is translated.");
171
172     $path = 'admin/config/regional/translate/';
173     $search = [
174       'langcode' => 'fr',
175       // Limit to only translated strings to ensure that database ordering does
176       // not break the test.
177       'translation' => 'translated',
178     ];
179     $this->drupalPostForm($path, $search, t('Filter'));
180     // Plural values for the langcode fr.
181     $this->assertText('@count seconde');
182     $this->assertText('@count secondes');
183
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.
191     $search = [
192       'string' => '1 second',
193       'langcode' => 'fr',
194     ];
195     $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
196
197     // Save complete translations for the string in langcode fr.
198     $edit = [
199       "strings[$lid][translations][0]" => '1 seconde updated',
200       "strings[$lid][translations][1]" => '@count secondes updated',
201     ];
202     $this->drupalPostForm($path, $edit, t('Save translations'));
203
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.');
206
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
209     // second or minute.
210     $this->adminUser->set('created', time() - 2)->save();
211     $this->drupalGet('user');
212     $this->assertText("secondes updated", "'Member for' text is translated.");
213   }
214
215   /**
216    * Tests plural editing and export functionality.
217    */
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(), [
222       'langcode' => 'fr',
223     ]);
224     $this->importPoFile($this->getPoFileWithComplexPlural(), [
225       'langcode' => 'hr',
226     ]);
227
228     // Get the French translations.
229     $this->drupalPostForm('admin/config/regional/translate/export', [
230       'langcode' => 'fr',
231     ], t('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.');
238
239     // Get the Croatian translations.
240     $this->drupalPostForm('admin/config/regional/translate/export', [
241       'langcode' => 'hr',
242     ], t('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.');
249
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");
254
255     // Look up editing page for this plural string and check fields.
256     $path = 'admin/config/regional/translate/';
257     $search = [
258       'langcode' => 'hr',
259     ];
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');
266
267     // Plural values for langcode hr.
268     $this->assertText('@count sat');
269     $this->assertText('@count sata');
270     $this->assertText('@count sati');
271
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();
274     $edit = [
275       "strings[$lid][translations][1]" => '@count sata edited',
276     ];
277     $this->drupalPostForm($path, $edit, t('Save translations'));
278
279     $search = [
280       'langcode' => 'fr',
281     ];
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');
287
288     // Edit langcode fr translations and see if that took effect.
289     $edit = [
290       "strings[$lid][translations][0]" => '@count heure edited',
291     ];
292     $this->drupalPostForm($path, $edit, t('Save translations'));
293
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.
301     $search = [
302       'string' => '1 day',
303       'langcode' => 'fr',
304     ];
305     $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
306
307     // Save complete translations for the string in langcode fr.
308     $edit = [
309       "strings[$lid][translations][0]" => '1 jour',
310       "strings[$lid][translations][1]" => '@count jours',
311     ];
312     $this->drupalPostForm($path, $edit, t('Save translations'));
313
314     // Save complete translations for the string in langcode hr.
315     $search = [
316       'string' => '1 day',
317       'langcode' => 'hr',
318     ];
319     $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
320
321     $edit = [
322       "strings[$lid][translations][0]" => '@count dan',
323       "strings[$lid][translations][1]" => '@count dana',
324       "strings[$lid][translations][2]" => '@count dana',
325     ];
326     $this->drupalPostForm($path, $edit, t('Save translations'));
327
328     // Get the French translations.
329     $this->drupalPostForm('admin/config/regional/translate/export', [
330       'langcode' => 'fr',
331     ], t('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.');
335
336     // Get the Croatian translations.
337     $this->drupalPostForm('admin/config/regional/translate/export', [
338       'langcode' => 'hr',
339     ], t('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.');
343   }
344
345   /**
346    * Imports a standalone .po file in a given language.
347    *
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.
352    */
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);
359   }
360
361   /**
362    * Returns a .po file with a simple plural formula.
363    */
364   public function getPoFileWithSimplePlural() {
365     return <<< EOF
366 msgid ""
367 msgstr ""
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"
373
374 msgid "1 hour"
375 msgid_plural "@count hours"
376 msgstr[0] "@count heure"
377 msgstr[1] "@count heures"
378
379 msgid "1 second"
380 msgid_plural "@count seconds"
381 msgstr[0] "@count seconde"
382 msgstr[1] "@count secondes"
383
384 msgid "Monday"
385 msgstr "lundi"
386 EOF;
387   }
388
389   /**
390    * Returns a .po file with a complex plural formula.
391    */
392   public function getPoFileWithComplexPlural() {
393     return <<< EOF
394 msgid ""
395 msgstr ""
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"
401
402 msgid "1 hour"
403 msgid_plural "@count hours"
404 msgstr[0] "@count sat"
405 msgstr[1] "@count sata"
406 msgstr[2] "@count sati"
407
408 msgid "Monday"
409 msgstr "Ponedjeljak"
410 EOF;
411   }
412
413   /**
414    * Returns a .po file with a missing plural formula.
415    */
416   public function getPoFileWithMissingPlural() {
417     return <<< EOF
418 msgid ""
419 msgstr ""
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"
424
425 msgid "Monday"
426 msgstr "lundi"
427 EOF;
428   }
429
430   /**
431    * Returns a .po file with a broken plural formula.
432    */
433   public function getPoFileWithBrokenPlural() {
434     return <<< EOF
435 msgid ""
436 msgstr ""
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"
442
443 msgid "Monday"
444 msgstr "Ponedjeljak"
445 EOF;
446   }
447
448 }