3 namespace Drupal\locale\Tests;
5 use Drupal\simpletest\WebTestBase;
6 use Drupal\Core\Language\LanguageInterface;
9 * Tests the import of locale files.
13 class LocaleImportFunctionalTest extends WebTestBase {
20 public static $modules = ['locale', 'dblog'];
23 * A user able to create languages and import translations.
25 * @var \Drupal\user\Entity\User
30 * A user able to create languages, import translations and access site
33 * @var \Drupal\user\Entity\User
35 protected $adminUserAccessSiteReports;
40 protected function setUp() {
43 // Copy test po files to the translations directory.
44 file_unmanaged_copy(__DIR__ . '/../../tests/test.de.po', 'translations://', FILE_EXISTS_REPLACE);
45 file_unmanaged_copy(__DIR__ . '/../../tests/test.xx.po', 'translations://', FILE_EXISTS_REPLACE);
47 $this->adminUser = $this->drupalCreateUser(['administer languages', 'translate interface', 'access administration pages']);
48 $this->adminUserAccessSiteReports = $this->drupalCreateUser(['administer languages', 'translate interface', 'access administration pages', 'access site reports']);
49 $this->drupalLogin($this->adminUser);
51 // Enable import of translations. By default this is disabled for automated
53 $this->config('locale.settings')
54 ->set('translation.import_enabled', TRUE)
59 * Test import of standalone .po files.
61 public function testStandalonePoFile() {
62 // Try importing a .po file.
63 $this->importPoFile($this->getPoFile(), [
66 $this->config('locale.settings');
67 // The import should automatically create the corresponding language.
68 $this->assertRaw(t('The language %language has been created.', ['%language' => 'French']), 'The language has been automatically created.');
70 // The import should have created 8 strings.
71 $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 8, '%update' => 0, '%delete' => 0]), 'The translation file was successfully imported.');
73 // This import should have saved plural forms to have 2 variants.
74 $locale_plurals = \Drupal::service('locale.plural.formula')->getNumberOfPlurals('fr');
75 $this->assertEqual(2, $locale_plurals, 'Plural number initialized.');
77 // Ensure we were redirected correctly.
78 $this->assertUrl(\Drupal::url('locale.translate_page', [], ['absolute' => TRUE]), [], 'Correct page redirection.');
80 // Try importing a .po file with invalid tags.
81 $this->importPoFile($this->getBadPoFile(), [
85 // The import should have created 1 string and rejected 2.
86 $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 1, '%update' => 0, '%delete' => 0]), 'The translation file was successfully imported.');
88 $skip_message = \Drupal::translation()->formatPlural(2, 'One translation string was skipped because of disallowed or malformed HTML. <a href=":url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.', [':url' => \Drupal::url('dblog.overview')]);
89 $this->assertRaw($skip_message, 'Unsafe strings were skipped.');
91 // Repeat the process with a user that can access site reports, and this
92 // time the different warnings must contain links to the log.
93 $this->drupalLogin($this->adminUserAccessSiteReports);
95 // Try importing a .po file with invalid tags.
96 $this->importPoFile($this->getBadPoFile(), [
100 $skip_message = \Drupal::translation()->formatPlural(2, 'One translation string was skipped because of disallowed or malformed HTML. <a href=":url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. <a href=":url">See the log</a> for details.', [':url' => \Drupal::url('dblog.overview')]);
101 $this->assertRaw($skip_message, 'Unsafe strings were skipped.');
103 // Check empty files import with a user that cannot access site reports..
104 $this->drupalLogin($this->adminUser);
105 // Try importing a zero byte sized .po file.
106 $this->importPoFile($this->getEmptyPoFile(), [
109 // The import should have created 0 string and rejected 0.
110 $this->assertRaw(t('One translation file could not be imported. See the log for details.'), 'The empty translation file import reported no translations imported.');
112 // Repeat the process with a user that can access site reports, and this
113 // time the different warnings must contain links to the log.
114 $this->drupalLogin($this->adminUserAccessSiteReports);
115 // Try importing a zero byte sized .po file.
116 $this->importPoFile($this->getEmptyPoFile(), [
119 // The import should have created 0 string and rejected 0.
120 $this->assertRaw(t('One translation file could not be imported. <a href=":url">See the log</a> for details.', [':url' => \Drupal::url('dblog.overview')]), 'The empty translation file import reported no translations imported.');
122 // Try importing a .po file which doesn't exist.
123 $name = $this->randomMachineName(16);
124 $this->drupalPostForm('admin/config/regional/translate/import', [
126 'files[file]' => $name,
128 $this->assertUrl(\Drupal::url('locale.translate_import', [], ['absolute' => TRUE]), [], 'Correct page redirection.');
129 $this->assertText(t('File to import not found.'), 'File to import not found message.');
131 // Try importing a .po file with overriding strings, and ensure existing
133 $this->importPoFile($this->getOverwritePoFile(), [
137 // The import should have created 1 string.
138 $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 1, '%update' => 0, '%delete' => 0]), 'The translation file was successfully imported.');
139 // Ensure string wasn't overwritten.
141 'string' => 'Montag',
143 'translation' => 'translated',
145 $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
146 $this->assertText(t('No strings available.'), 'String not overwritten by imported string.');
148 // This import should not have changed number of plural forms.
149 $locale_plurals = \Drupal::service('locale.plural.formula')->getNumberOfPlurals('fr');
150 $this->assertEqual(2, $locale_plurals, 'Plural numbers untouched.');
152 // Try importing a .po file with overriding strings, and ensure existing
153 // strings are overwritten.
154 $this->importPoFile($this->getOverwritePoFile(), [
156 'overwrite_options[not_customized]' => TRUE,
159 // The import should have updated 2 strings.
160 $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 0, '%update' => 2, '%delete' => 0]), 'The translation file was successfully imported.');
161 // Ensure string was overwritten.
163 'string' => 'Montag',
165 'translation' => 'translated',
167 $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
168 $this->assertNoText(t('No strings available.'), 'String overwritten by imported string.');
169 // This import should have changed number of plural forms.
170 $locale_plurals = \Drupal::service('locale.plural.formula')->reset()->getNumberOfPlurals('fr');
171 $this->assertEqual(3, $locale_plurals, 'Plural numbers changed.');
173 // Importing a .po file and mark its strings as customized strings.
174 $this->importPoFile($this->getCustomPoFile(), [
176 'customized' => TRUE,
179 // The import should have created 6 strings.
180 $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 6, '%update' => 0, '%delete' => 0]), 'The customized translation file was successfully imported.');
182 // The database should now contain 6 customized strings (two imported
183 // strings are not translated).
184 $count = db_query('SELECT COUNT(*) FROM {locales_target} WHERE customized = :custom', [':custom' => 1])->fetchField();
185 $this->assertEqual($count, 6, 'Customized translations successfully imported.');
187 // Try importing a .po file with overriding strings, and ensure existing
188 // customized strings are kept.
189 $this->importPoFile($this->getCustomOverwritePoFile(), [
191 'overwrite_options[not_customized]' => TRUE,
192 'overwrite_options[customized]' => FALSE,
195 // The import should have created 1 string.
196 $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 1, '%update' => 0, '%delete' => 0]), 'The customized translation file was successfully imported.');
197 // Ensure string wasn't overwritten.
199 'string' => 'januari',
201 'translation' => 'translated',
203 $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
204 $this->assertText(t('No strings available.'), 'Customized string not overwritten by imported string.');
206 // Try importing a .po file with overriding strings, and ensure existing
207 // customized strings are overwritten.
208 $this->importPoFile($this->getCustomOverwritePoFile(), [
210 'overwrite_options[not_customized]' => FALSE,
211 'overwrite_options[customized]' => TRUE,
214 // The import should have updated 2 strings.
215 $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 0, '%update' => 2, '%delete' => 0]), 'The customized translation file was successfully imported.');
216 // Ensure string was overwritten.
218 'string' => 'januari',
220 'translation' => 'translated',
222 $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
223 $this->assertNoText(t('No strings available.'), 'Customized string overwritten by imported string.');
228 * Test msgctxt context support.
230 public function testLanguageContext() {
231 // Try importing a .po file.
232 $this->importPoFile($this->getPoFileWithContext(), [
236 // We cast the return value of t() to string so as to retrieve the
237 // translated value, rendered as a string.
238 $this->assertIdentical((string) t('May', [], ['langcode' => 'hr', 'context' => 'Long month name']), 'Svibanj', 'Long month name context is working.');
239 $this->assertIdentical((string) t('May', [], ['langcode' => 'hr']), 'Svi.', 'Default context is working.');
243 * Test empty msgstr at end of .po file see #611786.
245 public function testEmptyMsgstr() {
248 // Try importing a .po file.
249 $this->importPoFile($this->getPoFileWithMsgstr(), [
250 'langcode' => $langcode,
253 $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 1, '%update' => 0, '%delete' => 0]), 'The translation file was successfully imported.');
254 $this->assertIdentical((string) t('Operations', [], ['langcode' => $langcode]), 'Műveletek', 'String imported and translated.');
256 // Try importing a .po file.
257 $this->importPoFile($this->getPoFileWithEmptyMsgstr(), [
258 'langcode' => $langcode,
259 'overwrite_options[not_customized]' => TRUE,
261 $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 0, '%update' => 0, '%delete' => 1]), 'The translation file was successfully imported.');
266 'langcode' => $langcode,
267 'translation' => 'untranslated',
269 $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
270 $this->assertText($str, 'Search found the string as untranslated.');
274 * Tests .po file import with configuration translation.
276 public function testConfigPoFile() {
277 // Values for translations to assert. Config key, original string,
278 // translation and config property name.
280 'system.maintenance' => [
281 '@site is currently under maintenance. We should be back shortly. Thank you for your patience.',
282 '@site karbantartás alatt áll. Rövidesen visszatérünk. Köszönjük a türelmet.',
285 'user.role.anonymous' => [
287 'Névtelen felhasználó',
292 // Add custom language for testing.
295 'predefined_langcode' => 'custom',
296 'langcode' => $langcode,
297 'label' => $this->randomMachineName(16),
298 'direction' => LanguageInterface::DIRECTION_LTR,
300 $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
302 // Check for the source strings we are going to translate. Adding the
303 // custom language should have made the process to export configuration
304 // strings to interface translation executed.
305 $locale_storage = $this->container->get('locale.storage');
306 foreach ($config_strings as $config_string) {
307 $string = $locale_storage->findString(['source' => $config_string[0], 'context' => '', 'type' => 'configuration']);
308 $this->assertTrue($string, 'Configuration strings have been created upon installation.');
311 // Import a .po file to translate.
312 $this->importPoFile($this->getPoFileWithConfig(), [
313 'langcode' => $langcode,
316 // Translations got recorded in the interface translation system.
317 foreach ($config_strings as $config_string) {
319 'string' => $config_string[0],
320 'langcode' => $langcode,
321 'translation' => 'all',
323 $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
324 $this->assertText($config_string[1], format_string('Translation of @string found.', ['@string' => $config_string[0]]));
327 // Test that translations got recorded in the config system.
328 $overrides = \Drupal::service('language.config_factory_override');
329 foreach ($config_strings as $config_key => $config_string) {
330 $override = $overrides->getOverride($langcode, $config_key);
331 $this->assertEqual($override->get($config_string[2]), $config_string[1]);
336 * Tests .po file import with user.settings configuration.
338 public function testConfigtranslationImportingPoFile() {
339 // Set the language code.
342 // Import a .po file to translate.
343 $this->importPoFile($this->getPoFileWithConfigDe(), [
344 'langcode' => $langcode]);
346 // Check that the 'Anonymous' string is translated.
347 $config = \Drupal::languageManager()->getLanguageConfigOverride($langcode, 'user.settings');
348 $this->assertEqual($config->get('anonymous'), 'Anonymous German');
352 * Test the translation are imported when a new language is created.
354 public function testCreatedLanguageTranslation() {
355 // Import a .po file to add de language.
356 $this->importPoFile($this->getPoFileWithConfigDe(), ['langcode' => 'de']);
358 // Get the language.entity.de label and check it's been translated.
359 $override = \Drupal::languageManager()->getLanguageConfigOverride('de', 'language.entity.de');
360 $this->assertEqual($override->get('label'), 'Deutsch');
364 * Helper function: import a standalone .po file in a given language.
366 * @param string $contents
367 * Contents of the .po file to import.
368 * @param array $options
369 * (optional) Additional options to pass to the translation import form.
371 public function importPoFile($contents, array $options = []) {
372 $name = \Drupal::service('file_system')->tempnam('temporary://', "po_") . '.po';
373 file_put_contents($name, $contents);
374 $options['files[file]'] = $name;
375 $this->drupalPostForm('admin/config/regional/translate/import', $options, t('Import'));
376 drupal_unlink($name);
380 * Helper function that returns a proper .po file.
382 public function getPoFile() {
386 "Project-Id-Version: Drupal 8\\n"
387 "MIME-Version: 1.0\\n"
388 "Content-Type: text/plain; charset=UTF-8\\n"
389 "Content-Transfer-Encoding: 8bit\\n"
390 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
393 msgid_plural "@count sheep"
394 msgstr[0] "un mouton"
395 msgstr[1] "@count moutons"
421 * Helper function that returns a empty .po file.
423 public function getEmptyPoFile() {
428 * Helper function that returns a bad .po file.
430 public function getBadPoFile() {
434 "Project-Id-Version: Drupal 8\\n"
435 "MIME-Version: 1.0\\n"
436 "Content-Type: text/plain; charset=UTF-8\\n"
437 "Content-Transfer-Encoding: 8bit\\n"
438 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
440 msgid "Save configuration"
441 msgstr "Enregistrer la configuration"
444 msgstr "modifier<img SRC="javascript:alert(\'xss\');">"
447 msgstr "supprimer<script>alert('xss');</script>"
453 * Helper function that returns a proper .po file for testing.
455 public function getOverwritePoFile() {
459 "Project-Id-Version: Drupal 8\\n"
460 "MIME-Version: 1.0\\n"
461 "Content-Type: text/plain; charset=UTF-8\\n"
462 "Content-Transfer-Encoding: 8bit\\n"
463 "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"
474 * Helper function that returns a .po file which strings will be marked
477 public function getCustomPoFile() {
481 "Project-Id-Version: Drupal 8\\n"
482 "MIME-Version: 1.0\\n"
483 "Content-Type: text/plain; charset=UTF-8\\n"
484 "Content-Transfer-Encoding: 8bit\\n"
485 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
488 msgid_plural "@count dogs"
490 msgstr[1] "@count chiens"
510 * Helper function that returns a .po file for testing customized strings.
512 public function getCustomOverwritePoFile() {
516 "Project-Id-Version: Drupal 8\\n"
517 "MIME-Version: 1.0\\n"
518 "Content-Type: text/plain; charset=UTF-8\\n"
519 "Content-Transfer-Encoding: 8bit\\n"
520 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
534 * Helper function that returns a .po file with context.
536 public function getPoFileWithContext() {
537 // Croatian (code hr) is one of the languages that have a different
538 // form for the full name and the abbreviated name for the month of May.
542 "Project-Id-Version: Drupal 8\\n"
543 "MIME-Version: 1.0\\n"
544 "Content-Type: text/plain; charset=UTF-8\\n"
545 "Content-Transfer-Encoding: 8bit\\n"
546 "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"
548 msgctxt "Long month name"
558 * Helper function that returns a .po file with an empty last item.
560 public function getPoFileWithEmptyMsgstr() {
564 "Project-Id-Version: Drupal 8\\n"
565 "MIME-Version: 1.0\\n"
566 "Content-Type: text/plain; charset=UTF-8\\n"
567 "Content-Transfer-Encoding: 8bit\\n"
568 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
577 * Helper function that returns a .po file with an empty last item.
579 public function getPoFileWithMsgstr() {
583 "Project-Id-Version: Drupal 8\\n"
584 "MIME-Version: 1.0\\n"
585 "Content-Type: text/plain; charset=UTF-8\\n"
586 "Content-Transfer-Encoding: 8bit\\n"
587 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
592 msgid "Will not appear in Drupal core, so we can ensure the test passes"
599 * Helper function that returns a .po file with configuration translations.
601 public function getPoFileWithConfig() {
605 "Project-Id-Version: Drupal 8\\n"
606 "MIME-Version: 1.0\\n"
607 "Content-Type: text/plain; charset=UTF-8\\n"
608 "Content-Transfer-Encoding: 8bit\\n"
609 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
611 msgid "@site is currently under maintenance. We should be back shortly. Thank you for your patience."
612 msgstr "@site karbantartás alatt áll. Rövidesen visszatérünk. Köszönjük a türelmet."
614 msgid "Anonymous user"
615 msgstr "Névtelen felhasználó"
621 * Helper function that returns a .po file with configuration translations.
623 public function getPoFileWithConfigDe() {
627 "Project-Id-Version: Drupal 8\\n"
628 "MIME-Version: 1.0\\n"
629 "Content-Type: text/plain; charset=UTF-8\\n"
630 "Content-Transfer-Encoding: 8bit\\n"
631 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
634 msgstr "Anonymous German"