first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
/**
* @file
* JavaScript for locale_test.module.
*
* @ignore
*/
Drupal.t("Standard Call t");
Drupal
.
t
(
"Whitespace Call t"
)
;
Drupal.t('Single Quote t');
Drupal.t('Single Quote \'Escaped\' t');
Drupal.t('Single Quote ' + 'Concat ' + 'strings ' + 't');
Drupal.t("Double Quote t");
Drupal.t("Double Quote \"Escaped\" t");
Drupal.t("Double Quote " + "Concat " + "strings " + "t");
Drupal.t("Context Unquoted t", {}, {context: "Context string unquoted"});
Drupal.t("Context Single Quoted t", {}, {'context': "Context string single quoted"});
Drupal.t("Context Double Quoted t", {}, {"context": "Context string double quoted"});
Drupal.t("Context !key Args t", {'!key': 'value'}, {context: "Context string"});
Drupal.formatPlural(1, "Standard Call plural", "Standard Call @count plural");
Drupal
.
formatPlural
(
1,
"Whitespace Call plural",
"Whitespace Call @count plural"
)
;
Drupal.formatPlural(1, 'Single Quote plural', 'Single Quote @count plural');
Drupal.formatPlural(1, 'Single Quote \'Escaped\' plural', 'Single Quote \'Escaped\' @count plural');
Drupal.formatPlural(1, "Double Quote plural", "Double Quote @count plural");
Drupal.formatPlural(1, "Double Quote \"Escaped\" plural", "Double Quote \"Escaped\" @count plural");
Drupal.formatPlural(1, "Context Unquoted plural", "Context Unquoted @count plural", {}, {context: "Context string unquoted"});
Drupal.formatPlural(1, "Context Single Quoted plural", "Context Single Quoted @count plural", {}, {'context': "Context string single quoted"});
Drupal.formatPlural(1, "Context Double Quoted plural", "Context Double Quoted @count plural", {}, {"context": "Context string double quoted"});
Drupal.formatPlural(1, "Context !key Args plural", "Context !key Args @count plural", {'!key': 'value'}, {context: "Context string"});
Drupal.formatPlural(1, "No count argument plural - singular", "No count argument plural - plural");

View File

@@ -0,0 +1,10 @@
name: 'Early translation test'
type: module
description: 'Support module for testing early bootstrap getting of annotations with translations.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,6 @@
services:
early_translation_test.authentication.early_translation_test:
class: Drupal\early_translation_test\Auth
arguments: ['@entity_type.manager']
tags:
- { name: authentication_provider, provider_id: 'early_translation_test', priority: 100 }

View File

@@ -0,0 +1,50 @@
<?php
namespace Drupal\early_translation_test;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Test authentication provider.
*/
class Auth implements AuthenticationProviderInterface {
/**
* The user storage.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* Constructs an authentication provider object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
// Authentication providers are called early during in the bootstrap.
// Getting the user storage used to result in a circular reference since
// translation involves a call to \Drupal\locale\LocaleLookup that tries to
// get the user roles.
// @see https://www.drupal.org/node/2241461
$this->userStorage = $entity_type_manager->getStorage('user');
}
/**
* {@inheritdoc}
*/
public function applies(Request $request) {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function authenticate(Request $request) {
return NULL;
}
}

View File

@@ -0,0 +1,13 @@
name: 'Locale test'
type: module
description: 'Support module for locale module testing.'
package: Testing
# version: '1.2'
hidden: true
'interface translation project': locale_test
'interface translation server pattern': core/modules/locale/test/test.%language.po
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,15 @@
<?php
/**
* @file
* Install, update and uninstall functions for the locale_test module.
*/
/**
* Implements hook_uninstall().
*/
function locale_test_uninstall() {
// Clear variables.
\Drupal::state()->delete('locale.test_system_info_alter');
\Drupal::state()->delete('locale.test_projects_alter');
}

View File

@@ -0,0 +1,223 @@
<?php
/**
* @file
* Simulate a custom module with a local po file.
*/
use Drupal\Core\Url;
use Drupal\Core\Extension\Extension;
use Drupal\Core\StreamWrapper\PublicStream;
/**
* Implements hook_system_info_alter().
*
* Make the test scripts to be believe this is not a hidden test module, but
* a regular custom module.
*/
function locale_test_system_info_alter(&$info, Extension $file, $type) {
// Only modify the system info if required.
// By default the locale_test modules are hidden and have a project specified.
// To test the module detection process by locale_project_list() the
// test modules should mimic a custom module. I.e. be non-hidden.
if (\Drupal::state()->get('locale.test_system_info_alter')) {
if ($file->getName() == 'locale_test' || $file->getName() == 'locale_test_translate') {
// Don't hide the module.
$info['hidden'] = FALSE;
}
}
// Alter the name and the core version of the project. This should not affect
// the locale project information.
if (\Drupal::state()->get('locale.test_system_info_alter_name_core')) {
if ($file->getName() == 'locale_test') {
$info['core'] = '8.6.7';
$info['name'] = 'locale_test_alter';
}
}
}
/**
* Implements hook_locale_translation_projects_alter().
*
* The translation status process by default checks the status of the installed
* projects. This function replaces the data of the installed modules by a
* predefined set of modules with fixed file names and release versions. Project
* names, versions, timestamps etc must be fixed because they must match the
* files created by the test script.
*
* The "locale.test_projects_alter" state variable must be set by the
* test script in order for this hook to take effect.
*/
function locale_test_locale_translation_projects_alter(&$projects) {
// Drupal core should not be translated. By overriding the server pattern we
// make sure that no translation for drupal core will be found and that the
// translation update system will not go out to l.d.o to check.
$projects['drupal']['server_pattern'] = 'translations://';
if (\Drupal::state()->get('locale.remove_core_project')) {
unset($projects['drupal']);
}
if (\Drupal::state()->get('locale.test_projects_alter')) {
// Instead of the default ftp.drupal.org we use the file system of the test
// instance to simulate a remote file location.
$url = Url::fromRoute('<front>', [], ['absolute' => TRUE])->toString();
$remote_url = $url . PublicStream::basePath() . '/remote/';
// Completely replace the project data with a set of test projects.
$projects = [
'contrib_module_one' => [
'name' => 'contrib_module_one',
'info' => [
'name' => 'Contributed module one',
'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
'package' => 'Other',
'version' => '8.x-1.1',
'project' => 'contrib_module_one',
'datestamp' => '1344471537',
'_info_file_ctime' => 1348767306,
],
'datestamp' => '1344471537',
'project_type' => 'module',
'project_status' => TRUE,
],
'contrib_module_two' => [
'name' => 'contrib_module_two',
'info' => [
'name' => 'Contributed module two',
'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
'package' => 'Other',
'version' => '8.x-2.0-beta4',
'project' => 'contrib_module_two',
'datestamp' => '1344471537',
'_info_file_ctime' => 1348767306,
],
'datestamp' => '1344471537',
'project_type' => 'module',
'project_status' => TRUE,
],
'contrib_module_three' => [
'name' => 'contrib_module_three',
'info' => [
'name' => 'Contributed module three',
'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
'package' => 'Other',
'version' => '8.x-1.0',
'project' => 'contrib_module_three',
'datestamp' => '1344471537',
'_info_file_ctime' => 1348767306,
],
'datestamp' => '1344471537',
'project_type' => 'module',
'project_status' => TRUE,
],
'locale_test' => [
'name' => 'locale_test',
'info' => [
'name' => 'Locale test',
'interface translation project' => 'locale_test',
'interface translation server pattern' => 'core/modules/locale/tests/test.%language.po',
'package' => 'Other',
'version' => NULL,
'project' => 'locale_test',
'_info_file_ctime' => 1348767306,
'datestamp' => 0,
],
'datestamp' => 0,
'project_type' => 'module',
'project_status' => TRUE,
],
'custom_module_one' => [
'name' => 'custom_module_one',
'info' => [
'name' => 'Custom module one',
'interface translation project' => 'custom_module_one',
'interface translation server pattern' => 'translations://custom_module_one.%language.po',
'package' => 'Other',
'version' => NULL,
'project' => 'custom_module_one',
'_info_file_ctime' => 1348767306,
'datestamp' => 0,
],
'datestamp' => 0,
'project_type' => 'module',
'project_status' => TRUE,
],
];
}
}
/**
* Implements hook_language_fallback_candidates_OPERATION_alter().
*/
function locale_test_language_fallback_candidates_locale_lookup_alter(array &$candidates, array $context) {
\Drupal::state()->set('locale.test_language_fallback_candidates_locale_lookup_alter_candidates', $candidates);
\Drupal::state()->set('locale.test_language_fallback_candidates_locale_lookup_alter_context', $context);
}
/**
* Implements hook_theme().
*/
function locale_test_theme() {
$return = [];
$return['locale_test_tokenized'] = [
'variable' => ['content' => ''],
];
return $return;
}
/**
* Implements hook_token_info().
*/
function locale_test_token_info() {
$info = [];
$info['types']['locale_test'] = [
'name' => t('Locale test'),
'description' => t('Locale test'),
];
$info['tokens']['locale_test']['security_test1'] = [
'type' => 'text',
'name' => t('Security test 1'),
];
$info['tokens']['locale_test']['security_test2'] = [
'type' => 'text',
'name' => t('Security test 2'),
];
return $info;
}
/**
* Implements hook_tokens().
*/
function locale_test_tokens($type, $tokens, array $data = [], array $options = []) {
$return = [];
if ($type == 'locale_test') {
foreach ($tokens as $name => $original) {
switch ($name) {
case 'security_test1':
$return[$original] = "javascript:alert('Hello!');";
break;
case 'security_test2':
$return[$original] = "<script>alert('Hello!');</script>";
break;
}
}
}
return $return;
}
/**
* Implements hook_countries_alter().
*/
function locale_test_countries_alter(&$countries) {
$countries['EB'] = 'Elbonia';
}

View File

@@ -0,0 +1 @@
{{ content }}

View File

@@ -0,0 +1,11 @@
name: 'Locale Test Development Release'
type: module
description: 'Helper module to test the behavior when the core version is a development release.'
package: Testing
# version: VERSION
hidden: true
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,42 @@
<?php
/**
* @file
* Simulate a Drupal version.
*/
use Drupal\Core\Extension\Extension;
/**
* Implements hook_system_info_alter().
*
* Change the core version number to a development one for testing.
* 8.0.0-alpha102-dev is the simulated version.
*/
function locale_test_development_release_system_info_alter(&$info, Extension $file, $type) {
if (isset($info['package']) && $info['package'] == 'Core') {
$info['version'] = '8.0.0-alpha102-dev';
}
}
/**
* Implements hook_locale_translation_projects_alter().
*
* Add a contrib module with a dev release to list of translatable modules.
*/
function locale_test_development_release_locale_translation_projects_alter(&$projects) {
$projects['contrib'] = [
'name' => 'contrib',
'info' => [
'name' => 'Contributed module',
'package' => 'Other',
'version' => '12.x-10.4-unstable11+14-dev',
'project' => 'contrib_module',
'datestamp' => '0',
'_info_file_ctime' => 1442933959,
],
'datestamp' => '0',
'project_type' => 'module',
'project_status' => TRUE,
];
}

View File

@@ -0,0 +1,13 @@
name: 'Locale test translate'
type: module
description: 'Translation test module for locale module testing.'
package: Testing
# version: '1.3'
hidden: true
'interface translation project': locale_test_translate
'interface translation server pattern': core/modules/locale/tests/test.%language.po
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,34 @@
<?php
/**
* @file
* Simulates a custom module with a local po file.
*/
use Drupal\Core\Extension\Extension;
/**
* Implements hook_system_info_alter().
*
* By default this modules is hidden but once enabled it behaves like a normal
* (not hidden) module. This hook implementation changes the .info.yml data by
* setting the hidden status to FALSE.
*/
function locale_test_translate_system_info_alter(&$info, Extension $file, $type) {
if ($file->getName() == 'locale_test_translate') {
// Don't hide the module.
$info['hidden'] = FALSE;
}
}
/**
* Implements hook_modules_installed().
*
* @see \Drupal\Tests\locale\Functional\LocaleConfigTranslationImportTest::testConfigTranslationWithForeignLanguageDefault
*/
function locale_test_translate_modules_installed($modules, $is_syncing) {
// Ensure that writing to configuration during install does not cause
// \Drupal\locale\LocaleConfigSubscriber to create incorrect translations due
// the configuration langcode and data being out-of-sync.
\Drupal::configFactory()->getEditable('locale_test_translate.settings')->set('key_set_during_install', TRUE)->save();
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for locale.
*
* @group locale
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,371 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
/**
* Tests translation update's effects on configuration translations.
*
* @group locale
*/
class LocaleConfigTranslationImportTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['language', 'locale_test_translate'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
}
/**
* Tests update changes configuration translations if enabled after language.
*/
public function testConfigTranslationImport(): void {
$admin_user = $this->drupalCreateUser([
'administer modules',
'administer site configuration',
'administer languages',
'access administration pages',
'administer permissions',
]);
$this->drupalLogin($admin_user);
// Add a language. The Afrikaans translation file of locale_test_translate
// (test.af.po) has been prepared with a configuration translation.
ConfigurableLanguage::createFromLangcode('af')->save();
// Enable locale module.
$this->container->get('module_installer')->install(['locale']);
$this->resetAll();
// Enable import of translations. By default this is disabled for automated
// tests.
$this->config('locale.settings')
->set('translation.import_enabled', TRUE)
->set('translation.use_source', LOCALE_TRANSLATION_USE_SOURCE_LOCAL)
->save();
// Add translation permissions now that the locale module has been enabled.
$edit = [
'authenticated[translate interface]' => 'translate interface',
];
$this->drupalGet('admin/people/permissions');
$this->submitForm($edit, 'Save permissions');
// Check and update the translation status. This will import the Afrikaans
// translations of locale_test_translate module.
$this->drupalGet('admin/reports/translations/check');
// Override the Drupal core translation status to be up to date.
// Drupal core should not be a subject in this test.
$status = locale_translation_get_status();
$status['drupal']['af']->type = 'current';
\Drupal::state()->set('locale.translation_status', $status);
$this->drupalGet('admin/reports/translations');
$this->submitForm([], 'Update translations');
// Check if configuration translations have been imported.
$override = \Drupal::languageManager()->getLanguageConfigOverride('af', 'system.maintenance');
// cSpell:disable-next-line
$this->assertEquals('Ons is tans besig met onderhoud op @site. Wees asseblief geduldig, ons sal binnekort weer terug wees.', $override->get('message'));
// Ensure that \Drupal\locale\LocaleConfigSubscriber::onConfigSave() works
// as expected during a configuration install that installs locale.
/** @var \Drupal\Core\Config\FileStorage $sync */
$sync = $this->container->get('config.storage.sync');
$this->copyConfig($this->container->get('config.storage'), $sync);
// Add our own translation to the config that will be imported.
$af_sync = $sync->createCollection('language.af');
$data = $af_sync->read('system.maintenance');
$data['message'] = 'Test af message';
$af_sync->write('system.maintenance', $data);
// Uninstall locale module.
$this->container->get('module_installer')->uninstall(['locale_test_translate']);
$this->container->get('module_installer')->uninstall(['locale']);
$this->resetAll();
$this->configImporter()->import();
$this->drupalGet('admin/reports/translations/check');
$status = locale_translation_get_status();
$status['drupal']['af']->type = 'current';
\Drupal::state()->set('locale.translation_status', $status);
$this->drupalGet('admin/reports/translations');
$this->submitForm([], 'Update translations');
// Check if configuration translations have been imported.
$override = \Drupal::languageManager()->getLanguageConfigOverride('af', 'system.maintenance');
$this->assertEquals('Test af message', $override->get('message'));
}
/**
* Tests update changes configuration translations if enabled after language.
*/
public function testConfigTranslationModuleInstall(): void {
// Enable locale, block and config_translation modules.
$this->container->get('module_installer')->install(['block', 'config_translation']);
$this->resetAll();
// The testing profile overrides locale.settings to disable translation
// import. Test that this override is in place.
$this->assertFalse($this->config('locale.settings')->get('translation.import_enabled'), 'Translations imports are disabled by default in the Testing profile.');
$admin_user = $this->drupalCreateUser([
'administer modules',
'administer site configuration',
'administer languages',
'access administration pages',
'administer permissions',
'translate configuration',
]);
$this->drupalLogin($admin_user);
// Enable import of translations. By default this is disabled for automated
// tests.
$this->config('locale.settings')
->set('translation.import_enabled', TRUE)
->set('translation.use_source', LOCALE_TRANSLATION_USE_SOURCE_LOCAL)
->save();
// Add predefined language.
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm(['predefined_langcode' => 'af'], 'Add language');
// Add the system branding block to the page.
$this->drupalPlaceBlock('system_branding_block', ['region' => 'header', 'id' => 'site_branding']);
$this->drupalGet('admin/config/system/site-information');
$this->submitForm(['site_slogan' => 'Test site slogan'], 'Save configuration');
$this->drupalGet('admin/config/system/site-information/translate/af/edit');
$this->submitForm([
'translation[config_names][system.site][slogan]' => 'Test site slogan in Afrikaans',
], 'Save translation');
// Get the front page and ensure that the translated configuration appears.
$this->drupalGet('af');
$this->assertSession()->pageTextContains('Test site slogan in Afrikaans');
$override = \Drupal::languageManager()->getLanguageConfigOverride('af', 'locale_test_translate.settings');
$this->assertEquals('Locale can translate Afrikaans', $override->get('translatable_default_with_translation'));
// Update test configuration.
$override
->set('translatable_no_default', 'This translation is preserved')
->set('translatable_default_with_translation', 'This translation is preserved')
->set('translatable_default_with_no_translation', 'This translation is preserved')
->save();
// Install any module.
$this->drupalGet('admin/modules');
$this->submitForm(['modules[dblog][enable]' => 'dblog'], 'Install');
$this->assertSession()->pageTextContains('Module Database Logging has been installed.');
// Get the front page and ensure that the translated configuration still
// appears.
$this->drupalGet('af');
$this->assertSession()->pageTextContains('Test site slogan in Afrikaans');
$this->rebuildContainer();
$override = \Drupal::languageManager()->getLanguageConfigOverride('af', 'locale_test_translate.settings');
$expected = [
'translatable_no_default' => 'This translation is preserved',
'translatable_default_with_translation' => 'This translation is preserved',
'translatable_default_with_no_translation' => 'This translation is preserved',
];
$this->assertEquals($expected, $override->get());
}
/**
* Tests removing a string from Locale deletes configuration translations.
*/
public function testLocaleRemovalAndConfigOverrideDelete(): void {
// Enable the locale module.
$this->container->get('module_installer')->install(['locale']);
$this->resetAll();
$admin_user = $this->drupalCreateUser([
'administer modules',
'administer site configuration',
'administer languages',
'access administration pages',
'administer permissions',
'translate interface',
]);
$this->drupalLogin($admin_user);
// Enable import of translations. By default this is disabled for automated
// tests.
$this->config('locale.settings')
->set('translation.import_enabled', TRUE)
->set('translation.use_source', LOCALE_TRANSLATION_USE_SOURCE_LOCAL)
->save();
// Add predefined language.
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm(['predefined_langcode' => 'af'], 'Add language');
$override = \Drupal::languageManager()->getLanguageConfigOverride('af', 'locale_test_translate.settings');
$this->assertEquals(['translatable_default_with_translation' => 'Locale can translate Afrikaans'], $override->get());
// Remove the string from translation to simulate a Locale removal. Note
// that is no current way of doing this in the UI.
$locale_storage = \Drupal::service('locale.storage');
$string = $locale_storage->findString(['source' => 'Locale can translate']);
\Drupal::service('locale.storage')->delete($string);
// Force a rebuild of config translations.
$count = \Drupal::service('locale.config_manager')->updateConfigTranslations(['locale_test_translate.settings'], ['af']);
$this->assertEquals(1, $count, 'Correct count of updated translations');
$override = \Drupal::languageManager()->getLanguageConfigOverride('af', 'locale_test_translate.settings');
$this->assertEquals([], $override->get());
$this->assertTrue($override->isNew(), 'The configuration override was deleted when the Locale string was deleted.');
}
/**
* Tests removing a string from Locale changes configuration translations.
*/
public function testLocaleRemovalAndConfigOverridePreserve(): void {
// Enable the locale module.
$this->container->get('module_installer')->install(['locale']);
$this->resetAll();
$admin_user = $this->drupalCreateUser([
'administer modules',
'administer site configuration',
'administer languages',
'access administration pages',
'administer permissions',
'translate interface',
]);
$this->drupalLogin($admin_user);
// Enable import of translations. By default this is disabled for automated
// tests.
$this->config('locale.settings')
->set('translation.import_enabled', TRUE)
->set('translation.use_source', LOCALE_TRANSLATION_USE_SOURCE_LOCAL)
->save();
// Add predefined language.
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm(['predefined_langcode' => 'af'], 'Add language');
$override = \Drupal::languageManager()->getLanguageConfigOverride('af', 'locale_test_translate.settings');
// Update test configuration.
$override
->set('translatable_no_default', 'This translation is preserved')
->set('translatable_default_with_no_translation', 'This translation is preserved')
->save();
$expected = [
'translatable_default_with_translation' => 'Locale can translate Afrikaans',
'translatable_no_default' => 'This translation is preserved',
'translatable_default_with_no_translation' => 'This translation is preserved',
];
$this->assertEquals($expected, $override->get());
// Set the translated string to empty.
$search = [
'string' => 'Locale can translate',
'langcode' => 'af',
'translation' => 'all',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = $textarea->getAttribute('name');
$edit = [
$lid => '',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
$override = \Drupal::languageManager()->getLanguageConfigOverride('af', 'locale_test_translate.settings');
$expected = [
'translatable_no_default' => 'This translation is preserved',
'translatable_default_with_no_translation' => 'This translation is preserved',
];
$this->assertEquals($expected, $override->get());
}
/**
* Tests setting a non-English language as default and importing configuration.
*/
public function testConfigTranslationWithNonEnglishLanguageDefault(): void {
/** @var \Drupal\Core\Extension\ModuleInstallerInterface $module_installer */
$module_installer = $this->container->get('module_installer');
ConfigurableLanguage::createFromLangcode('af')->save();
$module_installer->install(['locale']);
$this->resetAll();
/** @var \Drupal\locale\StringStorageInterface $local_storage */
$local_storage = $this->container->get('locale.storage');
$source_string = 'Locale can translate';
$translation_string = 'Locale can translate Afrikaans';
// Create a translation for the "Locale can translate" string, this string
// can be found in the "locale_test_translate" module's install config.
$source = $local_storage->createString([
'source' => $source_string,
])->save();
$local_storage->createTranslation([
'lid' => $source->getId(),
'language' => 'af',
'translation' => $translation_string,
])->save();
// Verify that we can find the newly added string translation, it is not a
// customized translation.
$translation = $local_storage->findTranslation([
'source' => $source_string,
'language' => 'af',
]);
$this->assertEquals($translation_string, $translation->getString());
$this->assertFalse((bool) $translation->customized);
// Uninstall the "locale_test_translate" module, verify that we can still
// find the string translation.
$module_installer->uninstall(['locale_test_translate']);
$this->resetAll();
$translation = $local_storage->findTranslation([
'source' => $source_string,
'language' => 'af',
]);
$this->assertEquals($translation_string, $translation->getString());
// Set the default language to "Afrikaans" and re-enable the
// "locale_test_translate" module.
$this->config('system.site')->set('default_langcode', 'af')->save();
$module_installer->install(['locale_test_translate']);
$this->resetAll();
// Verify that enabling the "locale_test_translate" module didn't cause
// the string translation to be overwritten.
$translation = $local_storage->findTranslation([
'source' => $source_string,
'language' => 'af',
]);
$this->assertEquals($translation_string, $translation->getString());
$this->assertFalse((bool) $translation->customized);
}
}

View File

@@ -0,0 +1,285 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\locale\StringStorageInterface;
use Drupal\Core\Language\LanguageInterface;
/**
* Tests translation of configuration strings.
*
* @group locale
*/
class LocaleConfigTranslationTest extends BrowserTestBase {
/**
* The language code used.
*
* @var string
*/
protected $langcode;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['locale', 'contact', 'contact_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* @var \Drupal\locale\StringStorageInterface
*/
protected StringStorageInterface $storage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Add a default locale storage for all these tests.
$this->storage = $this->container->get('locale.storage');
// Enable import of translations. By default this is disabled for automated
// tests.
$this->config('locale.settings')
->set('translation.import_enabled', TRUE)
->set('translation.use_source', LOCALE_TRANSLATION_USE_SOURCE_LOCAL)
->save();
// Add custom language.
$this->langcode = 'xx';
$admin_user = $this->drupalCreateUser([
'administer languages',
'access administration pages',
'translate interface',
'administer modules',
'access site-wide contact form',
'administer contact forms',
'administer site configuration',
]);
$this->drupalLogin($admin_user);
$name = $this->randomMachineName(16);
$edit = [
'predefined_langcode' => 'custom',
'langcode' => $this->langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add custom language');
// Set path prefix.
$edit = ["prefix[$this->langcode]" => $this->langcode];
$this->drupalGet('admin/config/regional/language/detection/url');
$this->submitForm($edit, 'Save configuration');
}
/**
* Tests basic configuration translation.
*/
public function testConfigTranslation(): void {
// Check that the maintenance message exists and create translation for it.
$source = '@site is currently under maintenance. We should be back shortly. Thank you for your patience.';
$string = $this->storage->findString(['source' => $source, 'context' => '', 'type' => 'configuration']);
$this->assertNotEmpty($string, 'Configuration strings have been created upon installation.');
// Translate using the UI so configuration is refreshed.
$message = $this->randomMachineName(20);
$search = [
'string' => $string->source,
'langcode' => $this->langcode,
'translation' => 'all',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = $textarea->getAttribute('name');
$edit = [
$lid => $message,
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
// Get translation and check we've only got the message.
$translation = \Drupal::languageManager()->getLanguageConfigOverride($this->langcode, 'system.maintenance')->get();
$this->assertCount(1, $translation, 'Got the right number of properties after translation.');
$this->assertEquals($message, $translation['message']);
// Check default medium date format exists and create a translation for it.
$string = $this->storage->findString(['source' => 'D, m/d/Y - H:i', 'context' => 'PHP date format', 'type' => 'configuration']);
$this->assertNotEmpty($string, 'Configuration date formats have been created upon installation.');
// Translate using the UI so configuration is refreshed.
$search = [
'string' => $string->source,
'langcode' => $this->langcode,
'translation' => 'all',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = $textarea->getAttribute('name');
$edit = [
$lid => 'D',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
$translation = \Drupal::languageManager()->getLanguageConfigOverride($this->langcode, 'core.date_format.medium')->get();
$this->assertEquals('D', $translation['pattern'], 'Got the right date format pattern after translation.');
// Formatting the date 8 / 27 / 1985 @ 13:37 EST with pattern D should
// display "Tue".
$formatted_date = $this->container->get('date.formatter')->format(494015820, $type = 'medium', NULL, 'America/New_York', $this->langcode);
$this->assertEquals('Tue', $formatted_date, 'Got the right formatted date using the date format translation pattern.');
// Assert strings from image module config are not available.
$string = $this->storage->findString(['source' => 'Medium (220×220)', 'context' => '', 'type' => 'configuration']);
$this->assertNull($string, 'Configuration strings have been created upon installation.');
// Enable the image module.
$this->drupalGet('admin/modules');
$this->submitForm(['modules[image][enable]' => "1"], 'Install');
$this->rebuildContainer();
$string = $this->storage->findString(['source' => 'Medium (220×220)', 'context' => '', 'type' => 'configuration']);
$this->assertNotEmpty($string, 'Configuration strings have been created upon installation.');
$locations = $string->getLocations();
// Check the configuration string has been created with the right location.
$this->assertArrayHasKey('configuration', $locations);
$this->assertArrayHasKey('image.style.medium', $locations['configuration']);
// Check the string is unique and has no translation yet.
$translations = $this->storage->getTranslations(['language' => $this->langcode, 'type' => 'configuration', 'name' => 'image.style.medium']);
$this->assertCount(1, $translations);
$translation = reset($translations);
$this->assertEquals($string->source, $translation->source);
$this->assertEmpty($translation->translation);
// Translate using the UI so configuration is refreshed.
$image_style_label = $this->randomMachineName(20);
$search = [
'string' => $string->source,
'langcode' => $this->langcode,
'translation' => 'all',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = $textarea->getAttribute('name');
$edit = [
$lid => $image_style_label,
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
// Check the right single translation has been created.
$translations = $this->storage->getTranslations(['language' => $this->langcode, 'type' => 'configuration', 'name' => 'image.style.medium']);
$translation = reset($translations);
$this->assertCount(1, $translations, 'Got only one translation for image configuration.');
$this->assertEquals($string->source, $translation->source);
$this->assertEquals($image_style_label, $translation->translation);
// Try more complex configuration data.
$translation = \Drupal::languageManager()->getLanguageConfigOverride($this->langcode, 'image.style.medium')->get();
$this->assertEquals($image_style_label, $translation['label'], 'Got the right translation for image style name after translation');
// Uninstall the module.
$this->drupalGet('admin/modules/uninstall');
$this->submitForm(['uninstall[image]' => "image"], 'Uninstall');
$this->submitForm([], 'Uninstall');
// Ensure that the translated configuration has been removed.
$override = \Drupal::languageManager()->getLanguageConfigOverride('xx', 'image.style.medium');
$this->assertTrue($override->isNew(), 'Translated configuration for image module removed.');
// Translate default category using the UI so configuration is refreshed.
$category_label = $this->randomMachineName(20);
$search = [
'string' => 'Website feedback',
'langcode' => $this->langcode,
'translation' => 'all',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = $textarea->getAttribute('name');
$edit = [
$lid => $category_label,
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
// Check if this category displayed in this language will use the
// translation. This test ensures the entity loaded from the request
// upcasting will already work.
$this->drupalGet($this->langcode . '/contact/feedback');
$this->assertSession()->pageTextContains($category_label);
// Check if the UI does not show the translated String.
$this->drupalGet('admin/structure/contact/manage/feedback');
$this->assertSession()->fieldValueEquals('edit-label', 'Website feedback');
}
/**
* Tests translatability of optional configuration in locale.
*/
public function testOptionalConfiguration(): void {
$this->assertNodeConfig(FALSE, FALSE);
// Enable the node module.
$this->drupalGet('admin/modules');
$this->submitForm(['modules[node][enable]' => "1"], 'Install');
$this->submitForm([], 'Continue');
$this->rebuildContainer();
$this->assertNodeConfig(TRUE, FALSE);
// Enable the views module (which node provides some optional config for).
$this->drupalGet('admin/modules');
$this->submitForm(['modules[views][enable]' => "1"], 'Install');
$this->rebuildContainer();
$this->assertNodeConfig(TRUE, TRUE);
}
/**
* Check that node configuration source strings are made available in locale.
*
* @param bool $required
* Whether to assume a sample of the required default configuration is
* present.
* @param bool $optional
* Whether to assume a sample of the optional default configuration is
* present.
*
* @internal
*/
protected function assertNodeConfig(bool $required, bool $optional): void {
// Check the required default configuration in node module.
$string = $this->storage->findString(['source' => 'Make content sticky', 'context' => '', 'type' => 'configuration']);
if ($required) {
$this->assertFalse($this->config('system.action.node_make_sticky_action')->isNew());
$this->assertNotEmpty($string, 'Node action text can be found with node module.');
}
else {
$this->assertTrue($this->config('system.action.node_make_sticky_action')->isNew());
$this->assertNull($string, 'Node action text can not be found without node module.');
}
// Check the optional default configuration in node module.
$string = $this->storage->findString(['source' => 'No front page content has been created yet.<br/>Follow the <a target="_blank" href="https://www.drupal.org/docs/user_guide/en/index.html">User Guide</a> to start building your site.', 'context' => '', 'type' => 'configuration']);
if ($optional) {
$this->assertFalse($this->config('views.view.frontpage')->isNew());
$this->assertNotEmpty($string, 'Node view text can be found with node and views modules.');
}
else {
$this->assertTrue($this->config('views.view.frontpage')->isNew());
$this->assertNull($string, 'Node view text can not be found without node and/or views modules.');
}
}
}

View File

@@ -0,0 +1,240 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Core\Language\LanguageInterface;
use Drupal\node\NodeInterface;
/**
* Tests multilingual support for content types and individual nodes.
*
* @group locale
*/
class LocaleContentTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node', 'locale'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Verifies that machine name fields are always LTR.
*/
public function testMachineNameLTR(): void {
// User to add and remove language.
$admin_user = $this->drupalCreateUser([
'administer languages',
'administer content types',
'access administration pages',
'administer site configuration',
]);
// Log in as admin.
$this->drupalLogin($admin_user);
// Verify that the machine name field is LTR for a new content type.
$this->drupalGet('admin/structure/types/add');
$type = $this->assertSession()->fieldExists('type');
$this->assertSame('ltr', $type->getAttribute('dir'));
// Install the Arabic language (which is RTL) and configure as the default.
$edit = [];
$edit['predefined_langcode'] = 'ar';
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add language');
$edit = [
'site_default_language' => 'ar',
];
$this->drupalGet('admin/config/regional/language');
$this->submitForm($edit, 'Save configuration');
// Verify that the machine name field is still LTR for a new content type.
$this->drupalGet('admin/structure/types/add');
$type = $this->assertSession()->fieldExists('type');
$this->assertSame('ltr', $type->getAttribute('dir'));
}
/**
* Tests if a content type can be set to multilingual and language is present.
*/
public function testContentTypeLanguageConfiguration(): void {
$type1 = $this->drupalCreateContentType();
$type2 = $this->drupalCreateContentType();
// User to add and remove language.
$admin_user = $this->drupalCreateUser([
'administer languages',
'administer content types',
'access administration pages',
]);
// User to create a node.
$web_user = $this->drupalCreateUser([
"create {$type1->id()} content",
"create {$type2->id()} content",
"edit any {$type2->id()} content",
]);
// Add custom language.
$this->drupalLogin($admin_user);
// Code for the language.
$langcode = 'xx';
// The English name for the language.
$name = $this->randomMachineName(16);
$edit = [
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add custom language');
// Set the content type to use multilingual support.
$this->drupalGet("admin/structure/types/manage/{$type2->id()}");
$this->assertSession()->pageTextContains('Language settings');
$edit = [
'language_configuration[language_alterable]' => TRUE,
];
$this->drupalGet("admin/structure/types/manage/{$type2->id()}");
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("The content type {$type2->label()} has been updated.");
$this->drupalLogout();
\Drupal::languageManager()->reset();
// Verify language selection is not present on the node add form.
$this->drupalLogin($web_user);
$this->drupalGet("node/add/{$type1->id()}");
// Verify language select list is not present.
$this->assertSession()->fieldNotExists('langcode[0][value]');
// Verify language selection appears on the node add form.
$this->drupalGet("node/add/{$type2->id()}");
// Verify language select list is present.
$this->assertSession()->fieldExists('langcode[0][value]');
// Ensure language appears.
$this->assertSession()->pageTextContains($name);
// Create a node.
$node_title = $this->randomMachineName();
$node_body = $this->randomMachineName();
$edit = [
'type' => $type2->id(),
'title' => $node_title,
'body' => [['value' => $node_body]],
'langcode' => $langcode,
];
$node = $this->drupalCreateNode($edit);
// Edit the content and ensure correct language is selected.
$path = 'node/' . $node->id() . '/edit';
$this->drupalGet($path);
$this->assertSession()->responseContains('<option value="' . $langcode . '" selected="selected">' . $name . '</option>');
// Ensure we can change the node language.
$edit = [
'langcode[0][value]' => 'en',
];
$this->drupalGet($path);
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains($node_title . ' has been updated.');
// Verify that the creation message contains a link to a node.
$xpath = $this->assertSession()->buildXPathQuery('//div[@data-drupal-messages]//a[contains(@href, :href)]', [
':href' => 'node/' . $node->id(),
]);
$this->assertSession()->elementExists('xpath', $xpath);
$this->drupalLogout();
}
/**
* Tests if a dir and lang tags exist in node's attributes.
*/
public function testContentTypeDirLang(): void {
$type = $this->drupalCreateContentType();
// User to add and remove language.
$admin_user = $this->drupalCreateUser([
'administer languages',
'administer content types',
'access administration pages',
]);
// User to create a node.
$web_user = $this->drupalCreateUser([
"create {$type->id()} content",
"edit own {$type->id()} content",
]);
// Log in as admin.
$this->drupalLogin($admin_user);
// Install Arabic language.
$edit = [];
$edit['predefined_langcode'] = 'ar';
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add language');
// Install Spanish language.
$edit = [];
$edit['predefined_langcode'] = 'es';
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add language');
\Drupal::languageManager()->reset();
// Set the content type to use multilingual support.
$this->drupalGet("admin/structure/types/manage/{$type->id()}");
$edit = [
'language_configuration[language_alterable]' => TRUE,
];
$this->drupalGet("admin/structure/types/manage/{$type->id()}");
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("The content type {$type->label()} has been updated.");
$this->drupalLogout();
// Log in as web user to add new node.
$this->drupalLogin($web_user);
// Create three nodes: English, Arabic and Spanish.
$nodes = [];
foreach (['en', 'es', 'ar'] as $langcode) {
$nodes[$langcode] = $this->drupalCreateNode([
'langcode' => $langcode,
'type' => $type->id(),
'promote' => NodeInterface::PROMOTED,
]);
}
// Check if English node does not have lang tag.
$this->drupalGet('node/' . $nodes['en']->id());
$element = $this->cssSelect('article[lang="en"]');
$this->assertEmpty($element, 'The lang tag has not been assigned to the English node.');
// Check if English node does not have dir tag.
$element = $this->cssSelect('article[dir="ltr"]');
$this->assertEmpty($element, 'The dir tag has not been assigned to the English node.');
// Check if Arabic node has lang="ar" & dir="rtl" tags.
$this->drupalGet('node/' . $nodes['ar']->id());
$element = $this->cssSelect('article[lang="ar"][dir="rtl"]');
$this->assertNotEmpty($element, 'The lang and dir tags have been assigned correctly to the Arabic node.');
// Check if Spanish node has lang="es" tag.
$this->drupalGet('node/' . $nodes['es']->id());
$element = $this->cssSelect('article[lang="es"]');
$this->assertNotEmpty($element, 'The lang tag has been assigned correctly to the Spanish node.');
// Check if Spanish node does not have dir="ltr" tag.
$element = $this->cssSelect('article[lang="es"][dir="ltr"]');
$this->assertEmpty($element, 'The dir tag has not been assigned to the Spanish node.');
}
}

View File

@@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Core\File\FileExists;
use Drupal\Tests\BrowserTestBase;
// cspell:ignore janvier lundi
/**
* Tests the exportation of locale files.
*
* @group locale
*/
class LocaleExportTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['locale'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user able to create languages and export translations.
*/
protected $adminUser = NULL;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser([
'administer languages',
'translate interface',
'access administration pages',
]);
$this->drupalLogin($this->adminUser);
// Copy test po files to the translations directory.
\Drupal::service('file_system')->copy(__DIR__ . '/../../../tests/test.de.po', 'translations://', FileExists::Replace);
\Drupal::service('file_system')->copy(__DIR__ . '/../../../tests/test.xx.po', 'translations://', FileExists::Replace);
}
/**
* Tests exportation of translations.
*/
public function testExportTranslation(): void {
$file_system = \Drupal::service('file_system');
// First import some known translations.
// This will also automatically add the 'fr' language.
$name = $file_system->tempnam('temporary://', "po_") . '.po';
file_put_contents($name, $this->getPoFile());
$this->drupalGet('admin/config/regional/translate/import');
$this->submitForm([
'langcode' => 'fr',
'files[file]' => $name,
], 'Import');
$file_system->unlink($name);
// Get the French translations.
$this->drupalGet('admin/config/regional/translate/export');
$this->submitForm(['langcode' => 'fr'], 'Export');
// Ensure we have a translation file.
$this->assertSession()->pageTextContains('# French translation of Drupal');
// Ensure our imported translations exist in the file.
$this->assertSession()->pageTextContains('msgstr "lundi"');
// Import some more French translations which will be marked as customized.
$name = $file_system->tempnam('temporary://', "po2_") . '.po';
file_put_contents($name, $this->getCustomPoFile());
$this->drupalGet('admin/config/regional/translate/import');
$this->submitForm([
'langcode' => 'fr',
'files[file]' => $name,
'customized' => 1,
], 'Import');
$file_system->unlink($name);
// Create string without translation in the locales_source table.
$this->container
->get('locale.storage')
->createString()
->setString('February')
->save();
// Export only customized French translations.
$this->drupalGet('admin/config/regional/translate/export');
$this->submitForm([
'langcode' => 'fr',
'content_options[not_customized]' => FALSE,
'content_options[customized]' => TRUE,
'content_options[not_translated]' => FALSE,
], 'Export');
// Ensure we have a translation file.
$this->assertSession()->pageTextContains('# French translation of Drupal');
// Ensure the customized translations exist in the file.
$this->assertSession()->pageTextContains('msgstr "janvier"');
// Ensure no untranslated strings exist in the file.
$this->assertSession()->responseNotContains('msgid "February"');
// Export only untranslated French translations.
$this->drupalGet('admin/config/regional/translate/export');
$this->submitForm([
'langcode' => 'fr',
'content_options[not_customized]' => FALSE,
'content_options[customized]' => FALSE,
'content_options[not_translated]' => TRUE,
], 'Export');
// Ensure we have a translation file.
$this->assertSession()->pageTextContains('# French translation of Drupal');
// Ensure no customized translations exist in the file.
$this->assertSession()->responseNotContains('msgstr "janvier"');
// Ensure the untranslated strings exist in the file, and with right quotes.
$this->assertSession()->responseContains($this->getUntranslatedString());
}
/**
* Tests exportation of translation template file.
*/
public function testExportTranslationTemplateFile(): void {
// Load an admin page with JavaScript so _drupal_add_library() fires at
// least once and _locale_parse_js_file() gets to run at least once so that
// the locales_source table gets populated with something.
$this->drupalGet('admin/config/regional/language');
// Get the translation template file.
$this->drupalGet('admin/config/regional/translate/export');
$this->submitForm([], 'Export');
// Ensure we have a translation file.
$this->assertSession()->pageTextContains('# LANGUAGE translation of PROJECT');
}
/**
* Helper function that returns a proper .po file.
*/
public function getPoFile() {
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 "Monday"
msgstr "lundi"
EOF;
}
/**
* Returns a .po file that will be marked as customized.
*/
public function getCustomPoFile() {
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 "January"
msgstr "janvier"
EOF;
}
/**
* Returns a .po file fragment with an untranslated string.
*
* @return string
* A .po file fragment with an untranslated string.
*/
public function getUntranslatedString() {
return <<< EOF
msgid "February"
msgstr ""
EOF;
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the locale functionality in the altered file settings form.
*
* @group locale
*/
class LocaleFileSystemFormTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['system'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$account = $this->drupalCreateUser(['administer site configuration']);
$this->drupalLogin($account);
}
/**
* Tests translation directory settings on the file settings form.
*/
public function testFileConfigurationPage(): void {
// By default there should be no setting for the translation directory.
$this->drupalGet('admin/config/media/file-system');
$this->assertSession()->fieldNotExists('translation_path');
// With locale module installed, the setting should appear.
$module_installer = $this->container->get('module_installer');
$module_installer->install(['locale']);
$this->rebuildContainer();
$this->drupalGet('admin/config/media/file-system');
$this->assertSession()->fieldExists('translation_path');
// The setting should persist.
$translation_path = $this->publicFilesDirectory . '/translations_changed';
$fields = [
'translation_path' => $translation_path,
];
$this->submitForm($fields, 'Save configuration');
$this->drupalGet('admin/config/media/file-system');
$this->assertSession()->fieldValueEquals('translation_path', $translation_path);
$this->assertEquals($this->config('locale.settings')->get('translation.path'), $translation_path);
}
}

View File

@@ -0,0 +1,679 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Core\Database\Database;
use Drupal\Core\File\FileExists;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
// cspell:ignore chien chiens deutsch januari lundi moutons műveletek svibanj
/**
* Tests the import of locale files.
*
* @group locale
*/
class LocaleImportFunctionalTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['locale', 'dblog'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user able to create languages and import translations.
*
* @var \Drupal\user\Entity\User
*/
protected $adminUser;
/**
* A user able to create languages, import translations, access site reports.
*
* @var \Drupal\user\Entity\User
*/
protected $adminUserAccessSiteReports;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Copy test po files to the translations directory.
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
$file_system->copy(__DIR__ . '/../../../tests/test.de.po', 'translations://', FileExists::Replace);
$file_system->copy(__DIR__ . '/../../../tests/test.xx.po', 'translations://', FileExists::Replace);
$this->adminUser = $this->drupalCreateUser([
'administer languages',
'translate interface',
'access administration pages',
]);
$this->adminUserAccessSiteReports = $this->drupalCreateUser([
'administer languages',
'translate interface',
'access administration pages',
'access site reports',
]);
$this->drupalLogin($this->adminUser);
// Enable import of translations. By default this is disabled for automated
// tests.
$this->config('locale.settings')
->set('translation.import_enabled', TRUE)
->set('translation.use_source', LOCALE_TRANSLATION_USE_SOURCE_LOCAL)
->save();
}
/**
* Tests import of standalone .po files.
*/
public function testStandalonePoFile(): void {
// Try importing a .po file.
$this->importPoFile($this->getPoFile(), [
'langcode' => 'fr',
]);
$this->config('locale.settings');
// The import should automatically create the corresponding language.
$this->assertSession()->pageTextContains("The language French has been created.");
// The import should have created 8 strings.
$this->assertSession()->pageTextContains("One translation file imported. 8 translations were added, 0 translations were updated and 0 translations were removed.");
// This import should have saved plural forms to have 2 variants.
$locale_plurals = \Drupal::service('locale.plural.formula')->getNumberOfPlurals('fr');
$this->assertEquals(2, $locale_plurals, 'Plural number initialized.');
// Ensure we were redirected correctly.
$this->assertSession()->addressEquals(Url::fromRoute('locale.translate_page'));
// Try importing a .po file with invalid tags.
$this->importPoFile($this->getBadPoFile(), [
'langcode' => 'fr',
]);
// The import should have created 1 string and rejected 2.
$this->assertSession()->pageTextContains("One translation file imported. 1 translations were added, 0 translations were updated and 0 translations were removed.");
$this->assertSession()->pageTextContains("2 translation strings were skipped because of disallowed or malformed HTML. See the log for details.");
// Repeat the process with a user that can access site reports, and this
// time the different warnings must contain links to the log.
$this->drupalLogin($this->adminUserAccessSiteReports);
// Try importing a .po file with invalid tags.
$this->importPoFile($this->getBadPoFile(), [
'langcode' => 'fr',
]);
$this->assertSession()->pageTextContains("2 translation strings were skipped because of disallowed or malformed HTML. See the log for details.");
// Check empty files import with a user that cannot access site reports..
$this->drupalLogin($this->adminUser);
// Try importing a zero byte sized .po file.
$this->importPoFile($this->getEmptyPoFile(), [
'langcode' => 'fr',
]);
// The import should have created 0 string and rejected 0.
$this->assertSession()->pageTextContains("One translation file could not be imported. See the log for details.");
// Repeat the process with a user that can access site reports, and this
// time the different warnings must contain links to the log.
$this->drupalLogin($this->adminUserAccessSiteReports);
// Try importing a zero byte sized .po file.
$this->importPoFile($this->getEmptyPoFile(), [
'langcode' => 'fr',
]);
// The import should have created 0 string and rejected 0.
$this->assertSession()->pageTextContains("One translation file could not be imported. See the log for details.");
// Try importing a .po file which doesn't exist.
$name = $this->randomMachineName(16);
$this->drupalGet('admin/config/regional/translate/import');
$this->submitForm([
'langcode' => 'fr',
'files[file]' => $name,
], 'Import');
$this->assertSession()->addressEquals(Url::fromRoute('locale.translate_import'));
$this->assertSession()->pageTextContains('File to import not found.');
// Try importing a .po file with overriding strings, and ensure existing
// strings are kept.
$this->importPoFile($this->getOverwritePoFile(), [
'langcode' => 'fr',
]);
// The import should have created 1 string.
$this->assertSession()->pageTextContains("One translation file imported. 1 translations were added, 0 translations were updated and 0 translations were removed.");
// Ensure string wasn't overwritten.
$search = [
'string' => 'Montag',
'langcode' => 'fr',
'translation' => 'translated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains('No strings available.');
// This import should not have changed number of plural forms.
$locale_plurals = \Drupal::service('locale.plural.formula')->getNumberOfPlurals('fr');
$this->assertEquals(2, $locale_plurals, 'Plural numbers untouched.');
// Try importing a .po file with overriding strings, and ensure existing
// strings are overwritten.
$this->importPoFile($this->getOverwritePoFile(), [
'langcode' => 'fr',
'overwrite_options[not_customized]' => TRUE,
]);
// The import should have updated 2 strings.
$this->assertSession()->pageTextContains("One translation file imported. 0 translations were added, 2 translations were updated and 0 translations were removed.");
// Ensure string was overwritten.
$search = [
'string' => 'Montag',
'langcode' => 'fr',
'translation' => 'translated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextNotContains('No strings available.');
// This import should have changed number of plural forms.
$locale_plurals = \Drupal::service('locale.plural.formula')->reset()->getNumberOfPlurals('fr');
$this->assertEquals(3, $locale_plurals, 'Plural numbers changed.');
// Importing a .po file and mark its strings as customized strings.
$this->importPoFile($this->getCustomPoFile(), [
'langcode' => 'fr',
'customized' => TRUE,
]);
// The import should have created 6 strings.
$this->assertSession()->pageTextContains("One translation file imported. 6 translations were added, 0 translations were updated and 0 translations were removed.");
// The database should now contain 6 customized strings (two imported
// strings are not translated).
$count = Database::getConnection()->select('locales_target')
->condition('customized', 1)
->countQuery()
->execute()
->fetchField();
$this->assertEquals(6, $count, 'Customized translations successfully imported.');
// Try importing a .po file with overriding strings, and ensure existing
// customized strings are kept.
$this->importPoFile($this->getCustomOverwritePoFile(), [
'langcode' => 'fr',
'overwrite_options[not_customized]' => TRUE,
'overwrite_options[customized]' => FALSE,
]);
// The import should have created 1 string.
$this->assertSession()->pageTextContains("One translation file imported. 1 translations were added, 0 translations were updated and 0 translations were removed.");
// Ensure string wasn't overwritten.
$search = [
'string' => 'januari',
'langcode' => 'fr',
'translation' => 'translated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains('No strings available.');
// Try importing a .po file with overriding strings, and ensure existing
// customized strings are overwritten.
$this->importPoFile($this->getCustomOverwritePoFile(), [
'langcode' => 'fr',
'overwrite_options[not_customized]' => FALSE,
'overwrite_options[customized]' => TRUE,
]);
// The import should have updated 2 strings.
$this->assertSession()->pageTextContains("One translation file imported. 0 translations were added, 2 translations were updated and 0 translations were removed.");
// Ensure string was overwritten.
$search = [
'string' => 'januari',
'langcode' => 'fr',
'translation' => 'translated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextNotContains('No strings available.');
}
/**
* Tests msgctxt context support.
*/
public function testLanguageContext(): void {
// Try importing a .po file.
$this->importPoFile($this->getPoFileWithContext(), [
'langcode' => 'hr',
]);
// We cast the return value of t() to string so as to retrieve the
// translated value, rendered as a string.
$this->assertSame('Svibanj', (string) t('May', [], ['langcode' => 'hr', 'context' => 'Long month name']), 'Long month name context is working.');
$this->assertSame('Svi.', (string) t('May', [], ['langcode' => 'hr']), 'Default context is working.');
}
/**
* Tests empty msgstr at end of .po file see #611786.
*/
public function testEmptyMsgstr(): void {
$langcode = 'hu';
// Try importing a .po file.
$this->importPoFile($this->getPoFileWithMsgstr(), [
'langcode' => $langcode,
]);
$this->assertSession()->pageTextContains("One translation file imported. 1 translations were added, 0 translations were updated and 0 translations were removed.");
$this->assertSame('Műveletek', (string) t('Operations', [], ['langcode' => $langcode]), 'String imported and translated.');
// Try importing a .po file.
$this->importPoFile($this->getPoFileWithEmptyMsgstr(), [
'langcode' => $langcode,
'overwrite_options[not_customized]' => TRUE,
]);
$this->assertSession()->pageTextContains("One translation file imported. 0 translations were added, 0 translations were updated and 1 translations were removed.");
$str = "Operations";
$search = [
'string' => $str,
'langcode' => $langcode,
'translation' => 'untranslated',
];
// Check that search finds the string as untranslated.
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains($str);
}
/**
* Tests .po file import with configuration translation.
*/
public function testConfigPoFile(): void {
// Values for translations to assert. Config key, original string,
// translation and config property name.
$config_strings = [
'system.maintenance' => [
'@site is currently under maintenance. We should be back shortly. Thank you for your patience.',
// cSpell:disable-next-line
'@site karbantartás alatt áll. Rövidesen visszatérünk. Köszönjük a türelmet.',
'message',
],
'user.role.anonymous' => [
'Anonymous user',
// cSpell:disable-next-line
'Névtelen felhasználó',
'label',
],
];
// Add custom language for testing.
$langcode = 'xx';
$edit = [
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $this->randomMachineName(16),
'direction' => LanguageInterface::DIRECTION_LTR,
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add custom language');
// Check for the source strings we are going to translate. Adding the
// custom language should have made the process to export configuration
// strings to interface translation executed.
$locale_storage = $this->container->get('locale.storage');
foreach ($config_strings as $config_string) {
$string = $locale_storage->findString(['source' => $config_string[0], 'context' => '', 'type' => 'configuration']);
$this->assertNotEmpty($string, 'Configuration strings have been created upon installation.');
}
// Import a .po file to translate.
$this->importPoFile($this->getPoFileWithConfig(), [
'langcode' => $langcode,
]);
// Translations got recorded in the interface translation system.
foreach ($config_strings as $config_string) {
$search = [
'string' => $config_string[0],
'langcode' => $langcode,
'translation' => 'all',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains($config_string[1]);
}
// Test that translations got recorded in the config system.
$overrides = \Drupal::service('language.config_factory_override');
foreach ($config_strings as $config_key => $config_string) {
$override = $overrides->getOverride($langcode, $config_key);
$this->assertEquals($override->get($config_string[2]), $config_string[1]);
}
}
/**
* Tests .po file import with user.settings configuration.
*/
public function testConfigTranslationImportingPoFile(): void {
// Set the language code.
$langcode = 'de';
// Import a .po file to translate.
$this->importPoFile($this->getPoFileWithConfigDe(), [
'langcode' => $langcode,
]);
// Check that the 'Anonymous' string is translated.
$config = \Drupal::languageManager()->getLanguageConfigOverride($langcode, 'user.settings');
$this->assertEquals('Anonymous German', $config->get('anonymous'));
}
/**
* Tests the translation are imported when a new language is created.
*/
public function testCreatedLanguageTranslation(): void {
// Import a .po file to add de language.
$this->importPoFile($this->getPoFileWithConfigDe(), ['langcode' => 'de']);
// Get the language.entity.de label and check it's been translated.
$override = \Drupal::languageManager()->getLanguageConfigOverride('de', 'language.entity.de');
$this->assertEquals('Deutsch', $override->get('label'));
}
/**
* Helper function: import a standalone .po file in a given language.
*
* @param string $contents
* Contents of the .po file to import.
* @param array $options
* (optional) Additional options to pass to the translation import form.
*/
public function importPoFile($contents, array $options = []) {
$file_system = \Drupal::service('file_system');
$name = $file_system->tempnam('temporary://', "po_") . '.po';
file_put_contents($name, $contents);
$options['files[file]'] = $name;
$this->drupalGet('admin/config/regional/translate/import');
$this->submitForm($options, 'Import');
$file_system->unlink($name);
}
/**
* Helper function that returns a proper .po file.
*/
public function getPoFile() {
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 "One sheep"
msgid_plural "@count sheep"
msgstr[0] "un mouton"
msgstr[1] "@count moutons"
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"
EOF;
}
/**
* Helper function that returns an empty .po file.
*/
public function getEmptyPoFile() {
return '';
}
/**
* Helper function that returns a bad .po file.
*/
public function getBadPoFile() {
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 "Save configuration"
msgstr "Enregistrer la configuration"
msgid "edit"
msgstr "modifier<img SRC="javascript:alert(\'xss\');">"
msgid "delete"
msgstr "supprimer<script>alert('xss');</script>"
EOF;
}
/**
* Helper function that returns a proper .po file for testing.
*/
public function getOverwritePoFile() {
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 "Monday"
msgstr "Montag"
msgid "Day"
msgstr "Jour"
EOF;
}
/**
* Returns a .po file that will be marked as customized.
*/
public function getCustomPoFile() {
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 "One dog"
msgid_plural "@count dogs"
msgstr[0] "un chien"
msgstr[1] "@count chiens"
msgid "January"
msgstr "janvier"
msgid "February"
msgstr "février"
msgid "March"
msgstr "mars"
msgid "April"
msgstr "avril"
msgid "June"
msgstr "juin"
EOF;
}
/**
* Helper function that returns a .po file for testing customized strings.
*/
public function getCustomOverwritePoFile() {
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 "January"
msgstr "januari"
msgid "February"
msgstr "februari"
msgid "July"
msgstr "juillet"
EOF;
}
/**
* Helper function that returns a .po file with context.
*/
public function getPoFileWithContext() {
// Croatian (code hr) is one of the languages that have a different
// form for the full name and the abbreviated name for the month of May.
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"
msgctxt "Long month name"
msgid "May"
msgstr "Svibanj"
msgid "May"
msgstr "Svi."
EOF;
}
/**
* Helper function that returns a .po file with an empty last item.
*/
public function getPoFileWithEmptyMsgstr() {
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 "Operations"
msgstr ""
EOF;
}
/**
* Helper function that returns a .po file with an empty last item.
*/
public function getPoFileWithMsgstr() {
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 "Operations"
msgstr "Műveletek"
msgid "Will not appear in Drupal core, so we can ensure the test passes"
msgstr ""
EOF;
}
/**
* Helper function that returns a .po file with configuration translations.
*/
public function getPoFileWithConfig() {
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 "@site is currently under maintenance. We should be back shortly. Thank you for your patience."
msgstr "@site karbantartás alatt áll. Rövidesen visszatérünk. Köszönjük a türelmet."
msgid "Anonymous user"
msgstr "Névtelen felhasználó"
EOF;
}
/**
* Helper function that returns a .po file with configuration translations.
*/
public function getPoFileWithConfigDe() {
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 "Anonymous"
msgstr "Anonymous German"
msgid "German"
msgstr "Deutsch"
EOF;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Test installation of Locale module.
*
* @group locale
*/
class LocaleInstallTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'file',
'language',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests Locale install message.
*/
public function testLocaleInstallMessage(): void {
$admin_user = $this->drupalCreateUser([
'access administration pages',
'administer modules',
]);
$this->drupalLogin($admin_user);
$edit = [];
$edit['modules[locale][enable]'] = 'locale';
$this->drupalGet('admin/modules');
$this->submitForm($edit, 'Install');
$this->assertSession()->statusMessageContains('available translations', 'status');
}
}

View File

@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Component\Gettext\PoItem;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Tests\BrowserTestBase;
// cspell:ignore descripcion mostrar
/**
* Tests parsing js files for translatable strings.
*
* @group locale
*/
class LocaleJavascriptTranslationTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['locale', 'locale_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
public function testFileParsing(): void {
// This test is for ensuring that the regular expression in
// _locale_parse_js_file() finds translatable source strings in all valid
// JavaScript syntax regardless of the coding style used, especially with
// respect to optional whitespace, line breaks, etc.
// - We test locale_test.js, because that is the one that contains a
// variety of whitespace styles.
$files[] = __DIR__ . '/../../locale_test.js';
foreach ($files as $filename) {
// Parse the file to look for source strings.
_locale_parse_js_file($filename);
// Get all of the source strings that were found.
$strings = $this->container
->get('locale.storage')
->getStrings([
'type' => 'javascript',
'name' => $filename,
]);
$source_strings = [];
foreach ($strings as $string) {
$source_strings[$string->source] = $string->context;
}
$etx = PoItem::DELIMITER;
// List of all strings that should be in the file.
$test_strings = [
'Standard Call t' => '',
'Whitespace Call t' => '',
'Single Quote t' => '',
"Single Quote \\'Escaped\\' t" => '',
'Single Quote Concat strings t' => '',
'Double Quote t' => '',
"Double Quote \\\"Escaped\\\" t" => '',
'Double Quote Concat strings t' => '',
'Context !key Args t' => 'Context string',
'Context Unquoted t' => 'Context string unquoted',
'Context Single Quoted t' => 'Context string single quoted',
'Context Double Quoted t' => 'Context string double quoted',
"Standard Call plural{$etx}Standard Call @count plural" => '',
"Whitespace Call plural{$etx}Whitespace Call @count plural" => '',
"Single Quote plural{$etx}Single Quote @count plural" => '',
"Single Quote \\'Escaped\\' plural{$etx}Single Quote \\'Escaped\\' @count plural" => '',
"Double Quote plural{$etx}Double Quote @count plural" => '',
"Double Quote \\\"Escaped\\\" plural{$etx}Double Quote \\\"Escaped\\\" @count plural" => '',
"Context !key Args plural{$etx}Context !key Args @count plural" => 'Context string',
"Context Unquoted plural{$etx}Context Unquoted @count plural" => 'Context string unquoted',
"Context Single Quoted plural{$etx}Context Single Quoted @count plural" => 'Context string single quoted',
"Context Double Quoted plural{$etx}Context Double Quoted @count plural" => 'Context string double quoted',
"No count argument plural - singular{$etx}No count argument plural - plural" => '',
];
// Assert that all strings were found properly.
foreach ($test_strings as $str => $context) {
// Make sure that the string was found in the file.
$this->assertTrue(isset($source_strings[$str]), "Found source string: $str");
// Make sure that the proper context was matched.
$this->assertArrayHasKey($str, $source_strings);
$this->assertSame($context, $source_strings[$str]);
}
$this->assertSameSize($test_strings, $source_strings, 'Found correct number of source strings.');
}
}
/**
* Assert translations JS is added before drupal.js, because it depends on it.
*/
public function testLocaleTranslationJsDependencies(): void {
// User to add and remove language.
$admin_user = $this->drupalCreateUser([
'administer languages',
'access administration pages',
'translate interface',
]);
// Add custom language.
$this->drupalLogin($admin_user);
// Code for the language.
$langcode = 'es';
// The English name for the language.
$name = $this->randomMachineName(16);
// The domain prefix.
$prefix = $langcode;
$edit = [
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add custom language');
// Set path prefix.
$edit = ["prefix[$langcode]" => $prefix];
$this->drupalGet('admin/config/regional/language/detection/url');
$this->submitForm($edit, 'Save configuration');
// This forces locale.admin.js string sources to be imported, which contains
// the next translation.
$this->drupalGet($prefix . '/admin/config/regional/translate');
// Translate a string in locale.admin.js to our new language.
$strings = \Drupal::service('locale.storage')
->getStrings([
'source' => 'Show description',
'type' => 'javascript',
'name' => 'core/modules/locale/locale.admin.js',
]);
$string = $strings[0];
$this->submitForm(['string' => 'Show description'], 'Filter');
$edit = ['strings[' . $string->lid . '][translations][0]' => 'Mostrar descripcion'];
$this->submitForm($edit, 'Save translations');
// Calculate the filename of the JS including the translations.
$js_translation_files = \Drupal::state()->get('locale.translation.javascript');
$js_filename = $prefix . '_' . $js_translation_files[$prefix] . '.js';
$content = $this->getSession()->getPage()->getContent();
$this->assertSession()->responseContains('core/misc/drupal.js');
$this->assertSession()->responseContains($js_filename);
// Assert translations JS is included before drupal.js.
$this->assertLessThan(strpos($content, 'core/misc/drupal.js'), strpos($content, $js_filename));
}
}

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Component\Gettext\PoItem;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\WaitTerminateTestTrait;
/**
* Tests LocaleLookup.
*
* @group locale
*/
class LocaleLocaleLookupTest extends BrowserTestBase {
use WaitTerminateTestTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['locale', 'locale_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// The \Drupal\locale\LocaleTranslation service stores localization cache
// data after the response is flushed to the client. We do not want to race
// with any string translations that may be saving from the login below.
$this->setWaitForTerminate();
// Change the language default object to different values.
ConfigurableLanguage::createFromLangcode('fr')->save();
$this->config('system.site')->set('default_langcode', 'fr')->save();
$this->drupalLogin($this->drupalCreateUser([
'administer modules',
]));
}
/**
* Tests that there are no circular dependencies.
*/
public function testCircularDependency(): void {
// Ensure that we can enable early_translation_test on a non-english site.
$this->drupalGet('admin/modules');
$this->submitForm(['modules[early_translation_test][enable]' => TRUE], 'Install');
$this->assertSession()->statusCodeEquals(200);
}
/**
* Tests language fallback defaults.
*/
public function testLanguageFallbackDefaults(): void {
$this->drupalGet('');
// Ensure state of fallback languages persisted by
// locale_test_language_fallback_candidates_locale_lookup_alter() is empty.
$this->assertEquals([], \Drupal::state()->get('locale.test_language_fallback_candidates_locale_lookup_alter_candidates'));
// Make sure there is enough information provided for alter hooks.
$context = \Drupal::state()->get('locale.test_language_fallback_candidates_locale_lookup_alter_context');
$this->assertEquals('fr', $context['langcode']);
$this->assertEquals('locale_lookup', $context['operation']);
}
/**
* Tests old plural style @count[number] fix.
*
* @dataProvider providerTestFixOldPluralStyle
*/
public function testFixOldPluralStyle($translation_value, $expected): void {
$string_storage = \Drupal::service('locale.storage');
$string = $string_storage->findString(['source' => 'Member for', 'context' => '']);
$lid = $string->getId();
$string_storage->createTranslation([
'lid' => $lid,
'language' => 'fr',
'translation' => $translation_value,
])->save();
_locale_refresh_translations(['fr'], [$lid]);
// Check that 'count[2]' was fixed for render value.
$this->drupalGet('');
$this->assertSession()->pageTextContains($expected);
// Check that 'count[2]' was saved for source value.
$translation = $string_storage->findTranslation(['language' => 'fr', 'lid' => $lid])->translation;
$this->assertSame($translation_value, $translation, 'Source value not changed');
$this->assertStringContainsString('@count[2]', $translation, 'Source value contains @count[2]');
}
/**
* Provides data for testFixOldPluralStyle().
*
* @return array
* An array of test data:
* - translation value
* - expected result
*/
public static function providerTestFixOldPluralStyle() {
return [
'non-plural translation' => ['@count[2] non-plural test', '@count[2] non-plural test'],
'plural translation' => ['@count[2] plural test' . PoItem::DELIMITER, '@count plural test'],
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
/**
* Tests installing in a different language with a dev version string.
*
* @group locale
*/
class LocaleNonInteractiveDevInstallTest extends LocaleNonInteractiveInstallTest {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function getVersionStringToTest() {
include_once $this->root . '/core/includes/install.core.inc';
$version = _install_get_version_info(\Drupal::VERSION);
return $version['major'] . '.' . $version['minor'] . '.x';
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests installing in a different language with a non-dev version string.
*
* @group locale
*/
class LocaleNonInteractiveInstallTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->markTestSkipped('Skipped due to major version-specific logic. See https://www.drupal.org/project/drupal/issues/3359322');
parent::setUp();
}
/**
* Gets the version string to use in the translation file.
*
* @return string
* The version string to test, for example, '8.0.0' or '8.6.x'.
*/
protected function getVersionStringToTest() {
include_once $this->root . '/core/includes/install.core.inc';
$version = _install_get_version_info(\Drupal::VERSION);
return $version['major'] . '.0.0';
}
/**
* {@inheritdoc}
*/
protected function installParameters() {
$parameters = parent::installParameters();
// Install Drupal in German.
$parameters['parameters']['langcode'] = 'de';
// Create a po file so we don't attempt to download one from
// localize.drupal.org and to have a test translation that will not change.
\Drupal::service('file_system')->mkdir($this->publicFilesDirectory . '/translations', NULL, TRUE);
$contents = <<<PO
msgid ""
msgstr ""
msgid "Enter the password that accompanies your username."
msgstr "Geben sie das Passwort für ihren Benutzernamen ein."
PO;
$version = $this->getVersionStringToTest();
file_put_contents($this->publicFilesDirectory . "/translations/drupal-{$version}.de.po", $contents);
return $parameters;
}
/**
* Tests that the expected translated text appears on the login screen.
*/
public function testInstallerTranslations(): void {
$this->drupalGet('user/login');
// cSpell:disable-next-line
$this->assertSession()->responseContains('Geben sie das Passwort für ihren Benutzernamen ein.');
}
}

View File

@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\Traits\Core\PathAliasTestTrait;
/**
* Tests you can configure a language for individual URL aliases.
*
* @group locale
* @group path
*/
class LocalePathTest extends BrowserTestBase {
use PathAliasTestTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node', 'locale', 'path', 'views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->config('system.site')->set('page.front', '/node')->save();
}
/**
* Tests if a language can be associated with a path alias.
*/
public function testPathLanguageConfiguration(): void {
// User to add and remove language.
$admin_user = $this->drupalCreateUser([
'administer languages',
'create page content',
'administer url aliases',
'create url aliases',
'access administration pages',
'access content overview',
]);
// Add custom language.
$this->drupalLogin($admin_user);
// Code for the language.
$langcode = 'xx';
// The English name for the language.
$name = $this->randomMachineName(16);
// The domain prefix.
$prefix = $langcode;
$edit = [
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add custom language');
// Set path prefix.
$edit = ["prefix[$langcode]" => $prefix];
$this->drupalGet('admin/config/regional/language/detection/url');
$this->submitForm($edit, 'Save configuration');
// Check that the "xx" front page is readily available because path prefix
// negotiation is pre-configured.
$this->drupalGet($prefix);
$this->assertSession()->pageTextContains('Welcome!');
// Create a node.
$node = $this->drupalCreateNode(['type' => 'page']);
// Create a path alias in default language (English).
$path = 'admin/config/search/path/add';
$english_path = $this->randomMachineName(8);
$edit = [
'path[0][value]' => '/node/' . $node->id(),
'alias[0][value]' => '/' . $english_path,
'langcode[0][value]' => 'en',
];
$this->drupalGet($path);
$this->submitForm($edit, 'Save');
// Create a path alias in new custom language.
$custom_language_path = $this->randomMachineName(8);
$edit = [
'path[0][value]' => '/node/' . $node->id(),
'alias[0][value]' => '/' . $custom_language_path,
'langcode[0][value]' => $langcode,
];
$this->drupalGet($path);
$this->submitForm($edit, 'Save');
// Confirm English language path alias works.
$this->drupalGet($english_path);
$this->assertSession()->pageTextContains($node->label());
// Confirm custom language path alias works.
$this->drupalGet($prefix . '/' . $custom_language_path);
$this->assertSession()->pageTextContains($node->label());
// Create a custom path.
$custom_path = $this->randomMachineName(8);
// Check priority of language for alias by source path.
$path_alias = $this->createPathAlias('/node/' . $node->id(), '/' . $custom_path, LanguageInterface::LANGCODE_NOT_SPECIFIED);
$lookup_path = $this->container->get('path_alias.manager')->getAliasByPath('/node/' . $node->id(), 'en');
$this->assertEquals('/' . $english_path, $lookup_path, 'English language alias has priority.');
// Same check for language 'xx'.
$lookup_path = $this->container->get('path_alias.manager')->getAliasByPath('/node/' . $node->id(), $prefix);
$this->assertEquals('/' . $custom_language_path, $lookup_path, 'Custom language alias has priority.');
$path_alias->delete();
// Create language nodes to check priority of aliases.
$first_node = $this->drupalCreateNode(['type' => 'page', 'promote' => 1, 'langcode' => 'en']);
$second_node = $this->drupalCreateNode(['type' => 'page', 'promote' => 1, 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED]);
// Assign a custom path alias to the first node with the English language.
$this->createPathAlias('/node/' . $first_node->id(), '/' . $custom_path, $first_node->language()->getId());
// Assign a custom path alias to second node with
// LanguageInterface::LANGCODE_NOT_SPECIFIED.
$this->createPathAlias('/node/' . $second_node->id(), '/' . $custom_path, $second_node->language()->getId());
// Test that both node titles link to our path alias.
$this->drupalGet('admin/content');
$custom_path_url = Url::fromUserInput('/' . $custom_path)->toString();
$this->assertSession()->elementExists('xpath', $this->assertSession()->buildXPathQuery('//a[@href=:href and normalize-space(text())=:title]', [
':href' => $custom_path_url,
':title' => $first_node->label(),
]));
$this->assertSession()->elementExists('xpath', $this->assertSession()->buildXPathQuery('//a[@href=:href and normalize-space(text())=:title]', [
':href' => $custom_path_url,
':title' => $second_node->label(),
]));
// Confirm that the custom path leads to the first node.
$this->drupalGet($custom_path);
$this->assertSession()->pageTextContains($first_node->label());
// Confirm that the custom path with prefix leads to the second node.
$this->drupalGet($prefix . '/' . $custom_path);
$this->assertSession()->pageTextContains($second_node->label());
}
}

View File

@@ -0,0 +1,487 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Component\Gettext\PoItem;
use Drupal\Core\Database\Database;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\Tests\BrowserTestBase;
// cspell:ignore heure heures jours lundi ponedjeljak
/**
* Tests plural handling for various languages.
*
* @group locale
*/
class LocalePluralFormatTest extends BrowserTestBase {
/**
* An admin user.
*
* @var \Drupal\user\Entity\User
*/
protected $adminUser;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['locale'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser([
'administer languages',
'translate interface',
'access administration pages',
]);
$this->drupalLogin($this->adminUser);
}
/**
* Tests locale_get_plural() and \Drupal::translation()->formatPlural().
*/
public function testGetPluralFormat(): void {
// Import some .po files with formulas to set up the environment.
// These will also add the languages to the system.
$this->importPoFile($this->getPoFileWithSimplePlural(), [
'langcode' => 'fr',
]);
$this->importPoFile($this->getPoFileWithComplexPlural(), [
'langcode' => 'hr',
]);
// Attempt to import some broken .po files as well to prove that these
// will not overwrite the proper plural formula imported above.
$this->importPoFile($this->getPoFileWithMissingPlural(), [
'langcode' => 'fr',
'overwrite_options[not_customized]' => TRUE,
]);
$this->importPoFile($this->getPoFileWithBrokenPlural(), [
'langcode' => 'hr',
'overwrite_options[not_customized]' => TRUE,
]);
// 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.
$plural_strings = [
// English is not imported in this case, so we assume built-in text
// and formulas.
'en' => [
0 => '1 hour',
1 => '@count hours',
],
'fr' => [
0 => '@count heure',
1 => '@count heures',
],
'hr' => [
0 => '@count sat',
1 => '@count sata',
2 => '@count sati',
],
// 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.
'hu' => [
0 => '1 hour',
-1 => '@count hours',
],
];
// Expected plural indexes precomputed base on the plural formulas with
// given $count value.
$plural_tests = [
'en' => [
1 => 0,
0 => 1,
5 => 1,
123 => 1,
235 => 1,
],
'fr' => [
1 => 0,
0 => 0,
5 => 1,
123 => 1,
235 => 1,
],
'hr' => [
1 => 0,
21 => 0,
0 => 2,
2 => 1,
8 => 2,
123 => 1,
235 => 2,
],
'hu' => [
1 => -1,
21 => -1,
0 => -1,
],
];
foreach ($plural_tests as $langcode => $tests) {
foreach ($tests as $count => $expected_plural_index) {
// Assert that the we get the right plural index.
$this->assertSame($expected_plural_index, locale_get_plural($count, $langcode), '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', (string) $count, $plural_strings[$langcode][$expected_plural_index]);
$this->assertSame($expected_plural_string, \Drupal::translation()->formatPlural($count, '@count hour', '@count hours', [], ['langcode' => $langcode])->render(), 'Plural translation of @count hour / @count hours for count ' . $count . ' in ' . $langcode . ' is ' . $expected_plural_string);
// DO NOT use translation to pass translated strings into
// PluralTranslatableMarkup::createFromTranslatedString() this way. It
// 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.
$translated_string = \Drupal::translation()->translate('@count hour' . PoItem::DELIMITER . '@count hours', [], ['langcode' => $langcode]);
$plural = PluralTranslatableMarkup::createFromTranslatedString($count, $translated_string, [], ['langcode' => $langcode]);
$this->assertSame($expected_plural_string, $plural->render());
}
}
}
/**
* Tests plural editing of DateFormatter strings.
*/
public function testPluralEditDateFormatter(): void {
// Import some .po files with formulas to set up the environment.
// These will also add the languages to the system.
$this->importPoFile($this->getPoFileWithSimplePlural(), [
'langcode' => 'fr',
]);
// Set French as the site default language.
$this->config('system.site')->set('default_langcode', 'fr')->save();
// 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();
$this->drupalGet('user');
// Member for time should be translated.
$this->assertSession()->pageTextContains("seconde");
$path = 'admin/config/regional/translate/';
$search = [
'langcode' => 'fr',
// Limit to only translated strings to ensure that database ordering does
// not break the test.
'translation' => 'translated',
];
$this->drupalGet($path);
$this->submitForm($search, 'Filter');
// Plural values for the langcode fr.
$this->assertSession()->pageTextContains('@count seconde');
$this->assertSession()->pageTextContains('@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.
\Drupal::translation()->formatPlural(1, '@count second', '@count seconds', [], ['langcode' => 'fr'])->render();
$lid = Database::getConnection()->select('locales_source', 'ls')
->fields('ls', ['lid'])
->condition('source', "@count second" . PoItem::DELIMITER . "@count seconds")
->condition('context', '')
->execute()
->fetchField();
// Look up editing page for this plural string and check fields.
$search = [
'string' => '@count second',
'langcode' => 'fr',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
// Save complete translations for the string in langcode fr.
$edit = [
"strings[$lid][translations][0]" => '@count seconde updated',
"strings[$lid][translations][1]" => '@count secondes updated',
];
$this->drupalGet($path);
$this->submitForm($edit, 'Save translations');
// User interface input for translating seconds should not be duplicated
$this->assertSession()->pageTextContainsOnce('@count seconds');
// 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();
$this->drupalGet('user');
$this->assertSession()->pageTextContains("secondes updated");
}
/**
* Tests plural editing and export functionality.
*/
public function testPluralEditExport(): void {
// Import some .po files with formulas to set up the environment.
// These will also add the languages to the system.
$this->importPoFile($this->getPoFileWithSimplePlural(), [
'langcode' => 'fr',
]);
$this->importPoFile($this->getPoFileWithComplexPlural(), [
'langcode' => 'hr',
]);
// Get the French translations.
$this->drupalGet('admin/config/regional/translate/export');
$this->submitForm(['langcode' => 'fr'], 'Export');
// Ensure we have a translation file.
$this->assertSession()->pageTextContains('# French translation of Drupal');
// Ensure our imported translations exist in the file.
$this->assertSession()->responseContains("msgid \"Monday\"\nmsgstr \"lundi\"");
// Check for plural export specifically.
$this->assertSession()->responseContains("msgid \"@count hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"@count heure\"\nmsgstr[1] \"@count heures\"");
// Get the Croatian translations.
$this->drupalGet('admin/config/regional/translate/export');
$this->submitForm(['langcode' => 'hr'], 'Export');
// Ensure we have a translation file.
$this->assertSession()->pageTextContains('# Croatian translation of Drupal');
// Ensure our imported translations exist in the file.
$this->assertSession()->responseContains("msgid \"Monday\"\nmsgstr \"Ponedjeljak\"");
// Check for plural export specifically.
$this->assertSession()->responseContains("msgid \"@count hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"@count sat\"\nmsgstr[1] \"@count sata\"\nmsgstr[2] \"@count sati\"");
// Check if the source appears on the translation page.
$this->drupalGet('admin/config/regional/translate');
$this->assertSession()->pageTextContains("@count hour");
$this->assertSession()->pageTextContains("@count hours");
// Look up editing page for this plural string and check fields.
$path = 'admin/config/regional/translate/';
$search = [
'langcode' => 'hr',
];
$this->drupalGet($path);
$this->submitForm($search, 'Filter');
// Labels for plural editing elements.
$this->assertSession()->pageTextContains('Singular form');
$this->assertSession()->pageTextContains('First plural form');
$this->assertSession()->pageTextContains('2. plural form');
$this->assertSession()->pageTextNotContains('3. plural form');
// Plural values for langcode hr.
$this->assertSession()->pageTextContains('@count sat');
$this->assertSession()->pageTextContains('@count sata');
$this->assertSession()->pageTextContains('@count sati');
$connection = Database::getConnection();
// Edit langcode hr translations and see if that took effect.
$lid = $connection->select('locales_source', 'ls')
->fields('ls', ['lid'])
->condition('source', "@count hour" . PoItem::DELIMITER . "@count hours")
->condition('context', '')
->execute()
->fetchField();
$edit = [
"strings[$lid][translations][1]" => '@count sata edited',
];
$this->drupalGet($path);
$this->submitForm($edit, 'Save translations');
$search = [
'langcode' => 'fr',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
// Plural values for the langcode fr.
$this->assertSession()->pageTextContains('@count heure');
$this->assertSession()->pageTextContains('@count heures');
$this->assertSession()->pageTextNotContains('2. plural form');
// Edit langcode fr translations and see if that took effect.
$edit = [
"strings[$lid][translations][0]" => '@count heure edited',
];
$this->drupalGet($path);
$this->submitForm($edit, 'Save translations');
// 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.
\Drupal::translation()->formatPlural(1, '@count day', '@count days', [], ['langcode' => 'fr'])->render();
$lid = $connection->select('locales_source', 'ls')
->fields('ls', ['lid'])
->condition('source', "@count day" . PoItem::DELIMITER . "@count days")
->condition('context', '')
->execute()
->fetchField();
// Look up editing page for this plural string and check fields.
$search = [
'string' => '@count day',
'langcode' => 'fr',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
// Save complete translations for the string in langcode fr.
$edit = [
"strings[$lid][translations][0]" => '@count jour',
"strings[$lid][translations][1]" => '@count jours',
];
$this->drupalGet($path);
$this->submitForm($edit, 'Save translations');
// Save complete translations for the string in langcode hr.
$search = [
'string' => '@count day',
'langcode' => 'hr',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$edit = [
"strings[$lid][translations][0]" => '@count dan',
"strings[$lid][translations][1]" => '@count dana',
"strings[$lid][translations][2]" => '@count dana',
];
$this->drupalGet($path);
$this->submitForm($edit, 'Save translations');
// Get the French translations.
$this->drupalGet('admin/config/regional/translate/export');
$this->submitForm(['langcode' => 'fr'], 'Export');
// Check for plural export specifically.
$this->assertSession()->responseContains("msgid \"@count hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"@count heure edited\"\nmsgstr[1] \"@count heures\"");
$this->assertSession()->responseContains("msgid \"@count day\"\nmsgid_plural \"@count days\"\nmsgstr[0] \"@count jour\"\nmsgstr[1] \"@count jours\"");
// Get the Croatian translations.
$this->drupalGet('admin/config/regional/translate/export');
$this->submitForm(['langcode' => 'hr'], 'Export');
// Check for plural export specifically.
$this->assertSession()->responseContains("msgid \"@count hour\"\nmsgid_plural \"@count hours\"\nmsgstr[0] \"@count sat\"\nmsgstr[1] \"@count sata edited\"\nmsgstr[2] \"@count sati\"");
$this->assertSession()->responseContains("msgid \"@count day\"\nmsgid_plural \"@count days\"\nmsgstr[0] \"@count dan\"\nmsgstr[1] \"@count dana\"\nmsgstr[2] \"@count dana\"");
}
/**
* Imports a standalone .po file in a given language.
*
* @param string $contents
* Contents of the .po file to import.
* @param array $options
* Additional options to pass to the translation import form.
*/
public function importPoFile($contents, array $options = []) {
$file_system = \Drupal::service('file_system');
$name = $file_system->tempnam('temporary://', "po_") . '.po';
file_put_contents($name, $contents);
$options['files[file]'] = $name;
$this->drupalGet('admin/config/regional/translate/import');
$this->submitForm($options, 'Import');
$file_system->unlink($name);
}
/**
* Returns a .po file with a simple plural formula.
*/
public function getPoFileWithSimplePlural() {
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 "@count hour"
msgid_plural "@count hours"
msgstr[0] "@count heure"
msgstr[1] "@count heures"
msgid "@count second"
msgid_plural "@count seconds"
msgstr[0] "@count seconde"
msgstr[1] "@count secondes"
msgid "Monday"
msgstr "lundi"
EOF;
}
/**
* Returns a .po file with a complex plural formula.
*/
public function getPoFileWithComplexPlural() {
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 "@count 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.
*/
public function getPoFileWithMissingPlural() {
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.
*/
public function getPoFileWithBrokenPlural() {
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;
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Core\Url;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\RequirementsPageTrait;
/**
* Adds and configures languages to check field schema definition.
*
* @group locale
*/
class LocaleTranslatedSchemaDefinitionTest extends BrowserTestBase {
use RequirementsPageTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['language', 'locale', 'node'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
ConfigurableLanguage::createFromLangcode('fr')->save();
$this->config('system.site')->set('default_langcode', 'fr')->save();
// Clear all caches so that the base field definition, its cache in the
// entity field manager, the t() cache, etc. are all cleared.
$this->resetAll();
}
/**
* Tests that translated field descriptions do not affect the update system.
*/
public function testTranslatedSchemaDefinition(): void {
/** @var \Drupal\locale\StringDatabaseStorage $stringStorage */
$stringStorage = \Drupal::service('locale.storage');
$source = $stringStorage->createString([
'source' => 'Revision ID',
])->save();
$stringStorage->createTranslation([
'lid' => $source->lid,
'language' => 'fr',
'translation' => 'Translated Revision ID',
])->save();
// Ensure that the field is translated when access through the API.
$this->assertEquals('Translated Revision ID', \Drupal::service('entity_field.manager')->getBaseFieldDefinitions('node')['vid']->getLabel());
// Assert there are no updates.
$this->assertFalse(\Drupal::service('entity.definition_update_manager')->needsUpdates());
}
/**
* Tests that translations do not affect the update system.
*/
public function testTranslatedUpdate(): void {
// Visit the update page to collect any strings that may be translatable.
$user = $this->drupalCreateUser(['administer software updates']);
$this->drupalLogin($user);
$update_url = Url::fromRoute('system.db_update')->setAbsolute()->toString();
// Collect strings from the PHP warning page, if applicable, as well as the
// main update page.
$this->drupalGet($update_url, ['external' => TRUE]);
$this->updateRequirementsProblem();
/** @var \Drupal\locale\StringDatabaseStorage $stringStorage */
$stringStorage = \Drupal::service('locale.storage');
$sources = $stringStorage->getStrings();
// Translate all source strings found.
foreach ($sources as $source) {
$stringStorage->createTranslation([
'lid' => $source->lid,
'language' => 'fr',
'translation' => $this->randomMachineName(100),
])->save();
}
// Ensure that there are no updates just due to translations. Check for
// markup and a link instead of specific text because text may be
// translated.
$this->drupalGet($update_url . '/selection', ['external' => TRUE]);
$this->assertSession()->statusMessageExists('status');
$this->assertSession()->linkByHrefNotExists('fr/update.php/run', 'No link to run updates.');
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Core\StreamWrapper\PublicStream;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests how translations are handled when a project gets updated.
*
* @group locale
*/
class LocaleTranslationChangeProjectVersionTest extends LocaleUpdateBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
\Drupal::moduleHandler()->loadInclude('locale', 'inc', 'locale.batch');
ConfigurableLanguage::createFromLangcode('de')->save();
\Drupal::state()->set('locale.test_projects_alter', TRUE);
\Drupal::state()->set('locale.remove_core_project', TRUE);
// Setup the environment.
$config = $this->config('locale.settings');
$public_path = PublicStream::basePath();
$this->setTranslationsDirectory($public_path . '/local');
$config
->set('translation.default_filename', '%project-%version.%language._po')
->set('translation.use_source', LOCALE_TRANSLATION_USE_SOURCE_LOCAL)
->save();
// This test uses .po files for the old translation file instead of the ._po
// files because locale_translate_get_interface_translation_files() (used to
// delete old translation files) only works with .po files.
// The new translation file uses _po.
// Old version: 8.x-1.0; New version: 8.x-1.1.
$this->makePoFile('remote/all/contrib_module_one', 'contrib_module_one-8.x-1.0.de.po', $this->timestampOld, []);
$this->makePoFile('remote/all/contrib_module_one', 'contrib_module_one-8.x-1.1.de._po', $this->timestampNew, []);
$this->makePoFile('local', 'contrib_module_one-8.x-1.0.de.po', $this->timestampOld, []);
// Initialize the projects status and change the project version to the old
// version. This makes the code update the module translation to the new
// version when the (batch) update script is triggered.
$status = locale_translation_get_status();
$status['contrib_module_one']['de']->version = '8.x-1.0';
\Drupal::keyValue('locale.translation_status')->setMultiple($status);
}
/**
* Tests update translations when project version changes.
*/
public function testUpdateImportSourceRemote(): void {
// Verify that the project status has the old version.
$status = locale_translation_get_status(['contrib_module_one']);
$this->assertEquals('8.x-1.0', $status['contrib_module_one']['de']->version);
// Verify that the old translation file exists and the new does not exist.
$this->assertFileExists('translations://contrib_module_one-8.x-1.0.de.po');
$this->assertFileDoesNotExist('translations://contrib_module_one-8.x-1.1.de._po');
// Run batch tasks.
$context = [];
locale_translation_batch_version_check('contrib_module_one', 'de', $context);
locale_translation_batch_status_check('contrib_module_one', 'de', [], $context);
locale_translation_batch_fetch_download('contrib_module_one', 'de', $context);
// Verify that the project status has the new version.
$status = locale_translation_get_status(['contrib_module_one']);
$this->assertEquals('8.x-1.1', $status['contrib_module_one']['de']->version);
// Verify that the old translation file was removed and the new was
// downloaded.
$this->assertFileDoesNotExist('translations://contrib_module_one-8.x-1.0.de.po');
$this->assertFileExists('translations://contrib_module_one-8.x-1.1.de._po');
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Core\StreamWrapper\PublicStream;
use Drupal\language\Entity\ConfigurableLanguage;
use org\bovigo\vfs\vfsStream;
/**
* Tests locale translation download.
*
* @group locale
*/
class LocaleTranslationDownloadTest extends LocaleUpdateBase {
/**
* The virtual file stream for storing translations.
*
* @var \org\bovigo\vfs\vfsStreamDirectory
*/
protected $translationsStream;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$moduleHandler = $this->container->get('module_handler');
$moduleHandler->loadInclude('locale', 'inc', 'locale.batch');
ConfigurableLanguage::createFromLangcode('de')->save();
// Let the translations:// stream wrapper point to a virtual file system to
// make it independent from the test environment.
$this->translationsStream = vfsStream::setup('translations');
\Drupal::configFactory()->getEditable('locale.settings')
->set('translation.path', $this->translationsStream->url())
->save();
}
/**
* Tests translation download from remote sources.
*/
public function testUpdateImportSourceRemote(): void {
// Provide remote and 'previously' downloaded translation file.
$this->setTranslationFiles();
vfsStream::create([
'contrib_module_one-8.x-1.1.de._po' => '__old_content__',
], $this->translationsStream);
$url = \Drupal::service('url_generator')->generateFromRoute('<front>', [], ['absolute' => TRUE]);
$uri = $url . PublicStream::basePath() . '/remote/all/contrib_module_one/contrib_module_one-8.x-1.1.de._po';
$source_file = (object) [
'uri' => $uri,
];
$result = locale_translation_download_source($source_file, 'translations://');
$this->assertEquals('translations://contrib_module_one-8.x-1.1.de._po', $result->uri);
$this->assertFileDoesNotExist('translations://contrib_module_one-8.x-1.1.de_0._po');
$this->assertFileExists('translations://contrib_module_one-8.x-1.1.de._po');
$this->assertStringNotContainsString('__old_content__', file_get_contents('translations://contrib_module_one-8.x-1.1.de._po'));
}
}

View File

@@ -0,0 +1,637 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Drupal\Core\Database\Database;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
use Drupal\Core\Language\LanguageInterface;
/**
* Tests the validation of translation strings and search results.
*
* @group locale
* @group #slow
*/
class LocaleTranslationUiTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['locale'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Enable interface translation to English.
*/
public function testEnglishTranslation(): void {
$admin_user = $this->drupalCreateUser([
'administer languages',
'access administration pages',
]);
$this->drupalLogin($admin_user);
$this->drupalGet('admin/config/regional/language/edit/en');
$this->submitForm(['locale_translate_english' => TRUE], 'Save language');
$this->assertSession()->linkByHrefExists('/admin/config/regional/translate?langcode=en', 0, 'Enabled interface translation to English.');
}
/**
* Adds a language and tests string translation by users with the appropriate permissions.
*/
public function testStringTranslation(): void {
// User to add and remove language.
$admin_user = $this->drupalCreateUser([
'administer languages',
'access administration pages',
]);
// User to translate and delete string.
$translate_user = $this->drupalCreateUser([
'translate interface',
'access administration pages',
]);
// Code for the language.
$langcode = 'xx';
// The English name for the language. This will be translated.
$name = 'Foo';
// This will be the translation of $name.
$translation = $this->randomMachineName(16);
$translation_to_en = $this->randomMachineName(16);
// Add custom language.
$this->drupalLogin($admin_user);
$edit = [
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add custom language');
// Add string.
t($name, [], ['langcode' => $langcode])->render();
// Reset locale cache.
$this->container->get('string_translation')->reset();
$this->assertSession()->responseContains('"edit-languages-' . $langcode . '-weight"');
// Ensure that test language was added.
$this->assertSession()->pageTextContains($name);
$this->drupalLogout();
// Add a whitespace at the end of string to ensure it is found.
$name_ws = $name . " ";
// Search for the name and translate it.
$this->drupalLogin($translate_user);
$search = [
'string' => $name_ws,
'langcode' => $langcode,
'translation' => 'untranslated',
];
// Check that search finds the string as untranslated.
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains($name);
// No t() here, it's surely not translated yet.
$this->assertSession()->pageTextContains($name);
// Verify that there is no way to translate the string to English.
$this->assertSession()->optionNotExists('edit-langcode', 'en');
$this->drupalLogout();
$this->drupalLogin($admin_user);
$this->drupalGet('admin/config/regional/language/edit/en');
$this->submitForm(['locale_translate_english' => TRUE], 'Save language');
$this->drupalLogout();
$this->drupalLogin($translate_user);
// Check that search finds the string as untranslated.
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains($name);
// Assume this is the only result, given the random name.
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = $textarea->getAttribute('name');
$edit = [
$lid => $translation,
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
$this->assertSession()->pageTextContains('The strings have been saved.');
$url_bits = explode('?', $this->getUrl());
$this->assertEquals(Url::fromRoute('locale.translate_page', [], ['absolute' => TRUE])->toString(), $url_bits[0], 'Correct page redirection.');
$search = [
'string' => $name,
'langcode' => $langcode,
'translation' => 'translated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains($translation);
$search = [
'string' => $name,
'langcode' => 'en',
'translation' => 'untranslated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = $textarea->getAttribute('name');
$edit = [
$lid => $translation_to_en,
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
$search = [
'string' => $name,
'langcode' => 'en',
'translation' => 'translated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains($translation_to_en);
$this->assertNotEquals($translation, $name);
$this->assertEquals($translation, t($name, [], ['langcode' => $langcode]), 't() works for non-English.');
// Refresh the locale() cache to get fresh data from t() below. We are in
// the same HTTP request and therefore t() is not refreshed by saving the
// translation above.
$this->container->get('string_translation')->reset();
// Now we should get the proper fresh translation from t().
$this->assertNotEquals($translation_to_en, $name);
$this->assertEquals($translation_to_en, t($name, [], ['langcode' => 'en']), 't() works for English.');
$this->assertTrue(t($name, [], ['langcode' => LanguageInterface::LANGCODE_SYSTEM]) == $name, 't() works for LanguageInterface::LANGCODE_SYSTEM.');
$search = [
'string' => $name,
'langcode' => 'en',
'translation' => 'untranslated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains('No strings available.');
// Test invalidation of 'rendered' cache tag after string translation.
$this->drupalLogout();
$this->drupalGet('xx/user/login');
$this->assertSession()->pageTextContains('Password');
$this->drupalLogin($translate_user);
$search = [
'string' => 'Password',
'langcode' => $langcode,
'translation' => 'untranslated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = $textarea->getAttribute('name');
$edit = [
$lid => 'Llamas are larger than frogs.',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
$this->drupalLogout();
$this->drupalGet('xx/user/login');
$this->assertSession()->pageTextContains('Llamas are larger than frogs.');
// Delete the language.
$this->drupalLogin($admin_user);
$path = 'admin/config/regional/language/delete/' . $langcode;
// This a confirm form, we do not need any fields changed.
$this->drupalGet($path);
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("The {$name} ({$langcode}) language has been removed.");
// Reload to remove $name.
$this->drupalGet($path);
// Verify that language is no longer found.
$this->assertSession()->statusCodeEquals(404);
$this->drupalLogout();
// Delete the string.
$this->drupalLogin($translate_user);
$search = [
'string' => $name,
'langcode' => 'en',
'translation' => 'translated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
// Assume this is the only result, given the random name.
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = $textarea->getAttribute('name');
$edit = [
$lid => '',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
$this->assertSession()->responseContains($name);
$this->drupalLogin($translate_user);
$search = [
'string' => $name,
'langcode' => 'en',
'translation' => 'untranslated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextNotContains('No strings available.');
}
/**
* Tests the rebuilding of JavaScript translation files on deletion.
*/
public function testJavaScriptTranslation(): void {
$user = $this->drupalCreateUser([
'translate interface',
'administer languages',
'access administration pages',
]);
$this->drupalLogin($user);
$config = $this->config('locale.settings');
$langcode = 'xx';
// The English name for the language. This will be translated.
$name = $this->randomMachineName(16);
// Add custom language.
$edit = [
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add custom language');
$this->container->get('language_manager')->reset();
// Build the JavaScript translation file.
// Retrieve the source string of the first string available in the
// {locales_source} table and translate it.
$query = Database::getConnection()->select('locales_source', 's');
$query->addJoin('INNER', 'locales_location', 'l', '[s].[lid] = [l].[lid]');
$source = $query->fields('s', ['source'])
->condition('l.type', 'javascript')
->range(0, 1)
->execute()
->fetchField();
$search = [
'string' => $source,
'langcode' => $langcode,
'translation' => 'all',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = $textarea->getAttribute('name');
$edit = [
$lid => $this->randomMachineName(),
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
// Trigger JavaScript translation parsing and building.
_locale_rebuild_js($langcode);
$locale_javascripts = \Drupal::state()->get('locale.translation.javascript', []);
$js_file = 'public://' . $config->get('javascript.directory') . '/' . $langcode . '_' . $locale_javascripts[$langcode] . '.js';
$this->assertFileExists($js_file);
// Test JavaScript translation rebuilding.
\Drupal::service('file_system')->delete($js_file);
$this->assertFileDoesNotExist($js_file);
_locale_rebuild_js($langcode);
$this->assertFileExists($js_file);
// Test if JavaScript translation contains a custom string override.
$string_override = $this->randomMachineName();
$settings = Settings::getAll();
$settings['locale_custom_strings_' . $langcode] = ['' => [$string_override => $string_override]];
// Recreate the settings static.
new Settings($settings);
_locale_rebuild_js($langcode);
$locale_javascripts = \Drupal::state()->get('locale.translation.javascript', []);
$js_file = 'public://' . $config->get('javascript.directory') . '/' . $langcode . '_' . $locale_javascripts[$langcode] . '.js';
$content = file_get_contents($js_file);
$this->assertStringContainsString('"' . $string_override . '":"' . $string_override . '"', $content);
}
/**
* Tests the validation of the translation input.
*/
public function testStringValidation(): void {
// User to add language and strings.
$admin_user = $this->drupalCreateUser([
'administer languages',
'access administration pages',
'translate interface',
]);
$this->drupalLogin($admin_user);
$langcode = 'xx';
// The English name for the language. This will be translated.
$name = $this->randomMachineName(16);
// These will be the invalid translations of $name.
$key = $this->randomMachineName(16);
$bad_translations[$key] = "<script>alert('xss');</script>" . $key;
$key = $this->randomMachineName(16);
$bad_translations[$key] = '<img SRC="javascript:alert(\'xss\');">' . $key;
$key = $this->randomMachineName(16);
$bad_translations[$key] = '<<SCRIPT>alert("xss");//<</SCRIPT>' . $key;
$key = $this->randomMachineName(16);
$bad_translations[$key] = "<BODY ONLOAD=alert('xss')>" . $key;
// Add custom language.
$edit = [
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add custom language');
// Add string.
t($name, [], ['langcode' => $langcode])->render();
// Reset locale cache.
$search = [
'string' => $name,
'langcode' => $langcode,
'translation' => 'all',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
// Find the edit path.
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = $textarea->getAttribute('name');
foreach ($bad_translations as $translation) {
$edit = [
$lid => $translation,
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
// Check for a form error on the textarea, which means the string was
// rejected as unsafe.
$this->assertSession()->elementAttributeContains('xpath', '//form[@id="locale-translate-edit-form"]//textarea', 'class', 'error');
$this->assertSession()->pageTextNotContains('The string has been saved.');
}
}
/**
* Tests translation search form.
*/
public function testStringSearch(): void {
// User to add and remove language.
$admin_user = $this->drupalCreateUser([
'administer languages',
'access administration pages',
]);
// User to translate and delete string.
$translate_user = $this->drupalCreateUser([
'translate interface',
'access administration pages',
]);
// Code for the language.
$langcode = 'xx';
// The English name for the language. This will be translated.
$name = $this->randomMachineName(16);
// This will be the translation of $name.
$translation = $this->randomMachineName(16);
// Add custom language.
$this->drupalLogin($admin_user);
$edit = [
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add custom language');
$edit = [
'predefined_langcode' => 'custom',
'langcode' => 'yy',
'label' => $this->randomMachineName(16),
'direction' => LanguageInterface::DIRECTION_LTR,
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add custom language');
// Add string.
t($name, [], ['langcode' => $langcode])->render();
// Reset locale cache.
$this->container->get('string_translation')->reset();
$this->drupalLogout();
// Search for the name.
$this->drupalLogin($translate_user);
$search = [
'string' => $name,
'langcode' => $langcode,
'translation' => 'all',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
// pageTextContains() seems to remove the input field where $name always
// could be found, so this is not a false assert. See how
// pageTextNotContains succeeds later.
$this->assertSession()->pageTextContains($name);
// Ensure untranslated string doesn't appear if searching on 'only
// translated strings'.
$search = [
'string' => $name,
'langcode' => $langcode,
'translation' => 'translated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains('No strings available.');
// Ensure untranslated string appears if searching on 'only untranslated
// strings'.
$search = [
'string' => $name,
'langcode' => $langcode,
'translation' => 'untranslated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextNotContains('No strings available.');
// Add translation.
// Assume this is the only result, given the random name.
// We save the lid from the path.
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = $textarea->getAttribute('name');
$edit = [
$lid => $translation,
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
// Ensure translated string does appear if searching on 'only
// translated strings'.
$search = [
'string' => $translation,
'langcode' => $langcode,
'translation' => 'translated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextNotContains('No strings available.');
// Ensure translated source string doesn't appear if searching on 'only
// untranslated strings'.
$search = [
'string' => $name,
'langcode' => $langcode,
'translation' => 'untranslated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains('No strings available.');
// Ensure translated string doesn't appear if searching on 'only
// untranslated strings'.
$search = [
'string' => $translation,
'langcode' => $langcode,
'translation' => 'untranslated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains('No strings available.');
// Ensure translated string does appear if searching on the custom language.
$search = [
'string' => $translation,
'langcode' => $langcode,
'translation' => 'all',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextNotContains('No strings available.');
// Ensure translated string doesn't appear if searching in System (English).
$search = [
'string' => $translation,
'langcode' => 'yy',
'translation' => 'all',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains('No strings available.');
// Search for a string that isn't in the system.
$unavailable_string = $this->randomMachineName(16);
$search = [
'string' => $unavailable_string,
'langcode' => $langcode,
'translation' => 'all',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains('No strings available.');
}
/**
* Tests that only changed strings are saved customized when edited.
*/
public function testUICustomizedStrings(): void {
$user = $this->drupalCreateUser([
'translate interface',
'administer languages',
'access administration pages',
]);
$this->drupalLogin($user);
ConfigurableLanguage::createFromLangcode('de')->save();
// Create test source string.
$string = $this->container->get('locale.storage')->createString([
'source' => $this->randomMachineName(100),
'context' => $this->randomMachineName(20),
])->save();
// Create translation for new string and save it as non-customized.
$translation = $this->container->get('locale.storage')->createTranslation([
'lid' => $string->lid,
'language' => 'de',
'translation' => $this->randomMachineName(100),
'customized' => 0,
])->save();
// Reset locale cache.
$this->container->get('string_translation')->reset();
// Ensure non-customized translation string does appear if searching
// non-customized translation.
$search = [
'string' => $string->getString(),
'langcode' => 'de',
'translation' => 'translated',
'customized' => '0',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains($translation->getString());
// Submit the translations without changing the translation.
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = $textarea->getAttribute('name');
$edit = [
$lid => $translation->getString(),
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
// Ensure unchanged translation string does appear if searching
// non-customized translation.
$search = [
'string' => $string->getString(),
'langcode' => 'de',
'translation' => 'translated',
'customized' => '0',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains($string->getString());
// Submit the translations with a new translation.
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = $textarea->getAttribute('name');
$edit = [
$lid => $this->randomMachineName(100),
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
// Ensure changed translation string does appear if searching customized
// translation.
$search = [
'string' => $string->getString(),
'langcode' => 'de',
'translation' => 'translated',
'customized' => '1',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains($string->getString());
}
}

View File

@@ -0,0 +1,322 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Core\Database\Database;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\StreamWrapper\PublicStream;
use Drupal\file\Entity\File;
use Drupal\Tests\BrowserTestBase;
// cspell:ignore februar januar juni marz
/**
* Base class for testing updates to string translations.
*/
abstract class LocaleUpdateBase extends BrowserTestBase {
/**
* Timestamp for an old translation.
*
* @var int
*/
protected $timestampOld;
/**
* Timestamp for a medium aged translation.
*
* @var int
*/
protected $timestampMedium;
/**
* Timestamp for a new translation.
*
* @var int
*/
protected $timestampNew;
/**
* Timestamp for current time.
*
* @var int
*/
protected $timestampNow;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['locale', 'locale_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Setup timestamps to identify old and new translation sources.
$this->timestampOld = \Drupal::time()->getRequestTime() - 300;
$this->timestampMedium = \Drupal::time()->getRequestTime() - 200;
$this->timestampNew = \Drupal::time()->getRequestTime() - 100;
$this->timestampNow = \Drupal::time()->getRequestTime();
// Enable import of translations. By default this is disabled for automated
// tests.
$this->config('locale.settings')
->set('translation.import_enabled', TRUE)
->set('translation.use_source', LOCALE_TRANSLATION_USE_SOURCE_LOCAL)
->save();
}
/**
* Sets the value of the default translations directory.
*
* @param string $path
* Path of the translations directory relative to the drupal installation
* directory.
*/
protected function setTranslationsDirectory($path) {
\Drupal::service('file_system')->prepareDirectory($path, FileSystemInterface::CREATE_DIRECTORY);
$this->config('locale.settings')->set('translation.path', $path)->save();
}
/**
* Adds a language.
*
* @param string $langcode
* The language code of the language to add.
*/
protected function addLanguage($langcode) {
$edit = ['predefined_langcode' => $langcode];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add language');
$this->container->get('language_manager')->reset();
$this->assertNotEmpty(\Drupal::languageManager()->getLanguage($langcode), "Language $langcode added.");
}
/**
* Creates a translation file and tests its timestamp.
*
* @param string $path
* Path of the file relative to the public file path.
* @param string $filename
* Name of the file to create.
* @param int $timestamp
* (optional) Timestamp to set the file to. Defaults to current time.
* @param array $translations
* (optional) Array of source/target value translation strings. Only
* singular strings are supported, no plurals. No double quotes are allowed
* in source and translations strings.
*/
protected function makePoFile($path, $filename, $timestamp = NULL, array $translations = []) {
$timestamp = $timestamp ? $timestamp : \Drupal::time()->getRequestTime();
$path = 'public://' . $path;
$text = '';
$po_header = <<<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"
EOF;
// Convert array of translations to Gettext source and translation strings.
if ($translations) {
foreach ($translations as $source => $target) {
$text .= 'msgid "' . $source . '"' . "\n";
$text .= 'msgstr "' . $target . '"' . "\n";
}
}
\Drupal::service('file_system')->prepareDirectory($path, FileSystemInterface::CREATE_DIRECTORY);
$fileUri = $path . '/' . $filename;
$file = File::create([
'uid' => 1,
'filename' => $filename,
'uri' => $fileUri,
'filemime' => 'text/x-gettext-translation',
'timestamp' => $timestamp,
]);
$file->setPermanent();
file_put_contents($file->getFileUri(), $po_header . $text);
touch(\Drupal::service('file_system')->realpath($file->getFileUri()), $timestamp);
$file->save();
$this->assertTrue(file_exists($fileUri));
$this->assertEquals($timestamp, filemtime($fileUri));
}
/**
* Setup the environment containing local and remote translation files.
*
* Update tests require a simulated environment for local and remote files.
* Normally remote files are located at a remote server (e.g. ftp.drupal.org).
* For testing we can not rely on this. A directory in the file system of the
* test site is designated for remote files and is addressed using an absolute
* URL. Because Drupal does not allow files with a po extension to be accessed
* (denied in .htaccess) the translation files get a _po extension. Another
* directory is designated for local translation files.
*
* The environment is set up with the following files. File creation times are
* set to create different variations in test conditions.
* contrib_module_one
* - remote file: timestamp new
* - local file: timestamp old
* contrib_module_two
* - remote file: timestamp old
* - local file: timestamp new
* contrib_module_three
* - remote file: timestamp old
* - local file: timestamp old
* custom_module_one
* - local file: timestamp new
* Time stamp of current translation set by setCurrentTranslations() is always
* timestamp medium. This makes it easy to predict which translation will be
* imported.
*/
protected function setTranslationFiles() {
$config = $this->config('locale.settings');
// A flag is set to let the locale_test module replace the project data with
// a set of test projects which match the below project files.
\Drupal::state()->set('locale.test_projects_alter', TRUE);
\Drupal::state()->set('locale.remove_core_project', FALSE);
// Setup the environment.
$public_path = PublicStream::basePath();
$this->setTranslationsDirectory($public_path . '/local');
$config->set('translation.default_filename', '%project-%version.%language._po')->save();
// Setting up sets of translations for the translation files.
$translations_one = ['January' => 'Januar_1', 'February' => 'Februar_1', 'March' => 'Marz_1'];
$translations_two = ['February' => 'Februar_2', 'March' => 'Marz_2', 'April' => 'April_2'];
$translations_three = ['April' => 'April_3', 'May' => 'Mai_3', 'June' => 'Juni_3'];
// Add a number of files to the local file system to serve as remote
// translation server and match the project definitions set in
// locale_test_locale_translation_projects_alter().
$this->makePoFile('remote/all/contrib_module_one', 'contrib_module_one-8.x-1.1.de._po', $this->timestampNew, $translations_one);
$this->makePoFile('remote/all/contrib_module_two', 'contrib_module_two-8.x-2.0-beta4.de._po', $this->timestampOld, $translations_two);
$this->makePoFile('remote/all/contrib_module_three', 'contrib_module_three-8.x-1.0.de._po', $this->timestampOld, $translations_three);
// Add a number of files to the local file system to serve as local
// translation files and match the project definitions set in
// locale_test_locale_translation_projects_alter().
$this->makePoFile('local', 'contrib_module_one-8.x-1.1.de._po', $this->timestampOld, $translations_one);
$this->makePoFile('local', 'contrib_module_two-8.x-2.0-beta4.de._po', $this->timestampNew, $translations_two);
$this->makePoFile('local', 'contrib_module_three-8.x-1.0.de._po', $this->timestampOld, $translations_three);
$this->makePoFile('local', 'custom_module_one.de.po', $this->timestampNew);
}
/**
* Sets up existing translations and their statuses in the database.
*/
protected function setCurrentTranslations() {
// Add non customized translations to the database.
$langcode = 'de';
$context = '';
$non_customized_translations = [
'March' => 'Marz',
'June' => 'Juni',
];
foreach ($non_customized_translations as $source => $translation) {
$string = $this->container->get('locale.storage')->createString([
'source' => $source,
'context' => $context,
])
->save();
$this->container->get('locale.storage')->createTranslation([
'lid' => $string->getId(),
'language' => $langcode,
'translation' => $translation,
'customized' => LOCALE_NOT_CUSTOMIZED,
])->save();
}
// Add customized translations to the database.
$customized_translations = [
'January' => 'Januar_customized',
'February' => 'Februar_customized',
'May' => 'Mai_customized',
];
foreach ($customized_translations as $source => $translation) {
$string = $this->container->get('locale.storage')->createString([
'source' => $source,
'context' => $context,
])
->save();
$this->container->get('locale.storage')->createTranslation([
'lid' => $string->getId(),
'language' => $langcode,
'translation' => $translation,
'customized' => LOCALE_CUSTOMIZED,
])->save();
}
// Add a state of current translations in locale_files.
$default = [
'langcode' => $langcode,
'uri' => '',
'timestamp' => $this->timestampMedium,
'last_checked' => $this->timestampMedium,
];
$data[] = [
'project' => 'contrib_module_one',
'filename' => 'contrib_module_one-8.x-1.1.de._po',
'version' => '8.x-1.1',
];
$data[] = [
'project' => 'contrib_module_two',
'filename' => 'contrib_module_two-8.x-2.0-beta4.de._po',
'version' => '8.x-2.0-beta4',
];
$data[] = [
'project' => 'contrib_module_three',
'filename' => 'contrib_module_three-8.x-1.0.de._po',
'version' => '8.x-1.0',
];
$data[] = [
'project' => 'custom_module_one',
'filename' => 'custom_module_one.de.po',
'version' => '',
];
$connection = Database::getConnection();
foreach ($data as $file) {
$file = array_merge($default, $file);
$connection->insert('locale_file')->fields($file)->execute();
}
}
/**
* Checks the translation of a string.
*
* @param string $source
* Translation source string.
* @param string $translation
* Translation to check. Use empty string to check for a non-existent
* translation.
* @param string $langcode
* Language code of the language to translate to.
* @param string $message
* (optional) A message to display with the assertion.
*/
protected function assertTranslation($source, $translation, $langcode, $message = '') {
$query = Database::getConnection()->select('locales_target', 'lt');
$query->innerJoin('locales_source', 'ls', '[ls].[lid] = [lt].[lid]');
$db_translation = $query->fields('lt', ['translation'])
->condition('ls.source', $source)
->condition('lt.language', $langcode)
->execute()
->fetchField();
$db_translation = $db_translation == FALSE ? '' : $db_translation;
$this->assertEquals($translation, $db_translation, $message ?: "Correct translation of $source ($langcode)");
}
}

View File

@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Core\Database\Database;
use Drupal\Tests\Traits\Core\CronRunTrait;
/**
* Tests for using cron to update project interface translations.
*
* @group locale
*/
class LocaleUpdateCronTest extends LocaleUpdateBase {
use CronRunTrait;
protected $batchOutput = [];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$admin_user = $this->drupalCreateUser([
'administer modules',
'administer site configuration',
'administer languages',
'access administration pages',
'translate interface',
]);
$this->drupalLogin($admin_user);
$this->addLanguage('de');
}
/**
* Tests interface translation update using cron.
*/
public function testUpdateCron(): void {
// Set a flag to let the locale_test module replace the project data with a
// set of test projects.
\Drupal::state()->set('locale.test_projects_alter', TRUE);
// Setup local and remote translations files.
$this->setTranslationFiles();
$this->config('locale.settings')->set('translation.default_filename', '%project-%version.%language._po')->save();
// Update translations using batch to ensure a clean test starting point.
$this->drupalGet('admin/reports/translations/check');
$this->drupalGet('admin/reports/translations');
$this->submitForm([], 'Update translations');
// Store translation status for comparison.
$initial_history = locale_translation_get_file_history();
// Prepare for test: Simulate new translations being available.
// Change the last updated timestamp of a translation file.
$contrib_module_two_uri = 'public://local/contrib_module_two-8.x-2.0-beta4.de._po';
touch(\Drupal::service('file_system')->realpath($contrib_module_two_uri), \Drupal::time()->getRequestTime());
// Prepare for test: Simulate that the file has not been checked for a long
// time. Set the last_check timestamp to zero.
$query = Database::getConnection()->update('locale_file');
$query->fields(['last_checked' => 0]);
$query->condition('project', 'contrib_module_two');
$query->condition('langcode', 'de');
$query->execute();
// Test: Disable cron update and verify that no tasks are added to the
// queue.
$edit = [
'update_interval_days' => 0,
];
$this->drupalGet('admin/config/regional/translate/settings');
$this->submitForm($edit, 'Save configuration');
// Execute locale cron tasks to add tasks to the queue.
locale_cron();
// Check whether no tasks are added to the queue.
$queue = \Drupal::queue('locale_translation', TRUE);
$this->assertEquals(0, $queue->numberOfItems(), 'Queue is empty');
// Test: Enable cron update and check if update tasks are added to the
// queue.
// Set cron update to Weekly.
$edit = [
'update_interval_days' => 7,
];
$this->drupalGet('admin/config/regional/translate/settings');
$this->submitForm($edit, 'Save configuration');
// Execute locale cron tasks to add tasks to the queue.
locale_cron();
// Check whether tasks are added to the queue.
// Expected tasks:
// - locale_translation_batch_version_check
// - locale_translation_batch_status_check
// - locale_translation_batch_status_finished.
$queue = \Drupal::queue('locale_translation', TRUE);
$this->assertEquals(3, $queue->numberOfItems(), 'Queue holds tasks for one project.');
$item = $queue->claimItem();
$queue->releaseItem($item);
$this->assertEquals('contrib_module_two', $item->data[1][0], 'Queue holds tasks for contrib module one.');
// Test: Run cron for a second time and check if tasks are not added to
// the queue twice.
locale_cron();
// Check whether no more tasks are added to the queue.
$queue = \Drupal::queue('locale_translation', TRUE);
$this->assertEquals(3, $queue->numberOfItems(), 'Queue holds tasks for one project.');
// Ensure last checked is updated to a greater time than the initial value.
sleep(1);
// Test: Execute cron and check if tasks are executed correctly.
// Run cron to process the tasks in the queue.
$this->cronRun();
drupal_static_reset('locale_translation_get_file_history');
$history = locale_translation_get_file_history();
$initial = $initial_history['contrib_module_two']['de'];
$current = $history['contrib_module_two']['de'];
// Verify that the translation of contrib_module_one is imported and
// updated.
$this->assertGreaterThan($initial->timestamp, $current->timestamp);
$this->assertGreaterThan($initial->last_checked, $current->last_checked);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Test for proper version fallback in case of a development release.
*
* @group language
*/
class LocaleUpdateDevelopmentReleaseTest extends BrowserTestBase {
protected static $modules = ['locale', 'locale_test_development_release'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
\Drupal::moduleHandler()->loadInclude('locale', 'inc', 'locale.compare');
$admin_user = $this->drupalCreateUser([
'administer modules',
'administer languages',
'access administration pages',
'translate interface',
]);
$this->drupalLogin($admin_user);
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm(['predefined_langcode' => 'hu'], 'Add language');
}
public function testLocaleUpdateDevelopmentRelease(): void {
$projects = locale_translation_build_projects();
$this->assertEquals('8.0.x', $projects['drupal']->info['version'], 'The branch of the core dev release.');
$this->assertEquals('12.x-10.x', $projects['contrib']->info['version'], 'The branch of the contrib module dev release.');
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Core\Url;
/**
* Tests for the user interface of project interface translations.
*
* @group locale
*/
class LocaleUpdateInterfaceTest extends LocaleUpdateBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['locale_test_translate'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$admin_user = $this->drupalCreateUser([
'administer modules',
'administer site configuration',
'administer languages',
'access administration pages',
'translate interface',
]);
$this->drupalLogin($admin_user);
}
/**
* Tests the user interfaces of the interface translation update system.
*
* Testing the Available updates summary on the side wide status page and the
* Available translation updates page.
*/
public function testInterface(): void {
// No language added.
// Check status page and Available translation updates page.
$this->drupalGet('admin/reports/status');
$this->assertSession()->pageTextNotContains('Translation update status');
$this->drupalGet('admin/reports/translations');
$this->assertSession()->pageTextContains("No translatable languages available. Add a language first.");
$this->assertSession()->linkByHrefExists(Url::fromRoute('entity.configurable_language.collection')->toString());
// Add German language.
$this->addLanguage('de');
// Override Drupal core translation status as 'up-to-date'.
$status = locale_translation_get_status();
$status['drupal']['de']->type = 'current';
\Drupal::keyValue('locale.translation_status')->set('drupal', $status['drupal']);
// One language added, all translations up to date.
$this->drupalGet('admin/reports/status');
$this->assertSession()->pageTextContains('Translation update status');
$this->assertSession()->pageTextContains('Up to date');
$this->drupalGet('admin/reports/translations');
$this->assertSession()->pageTextContains('All translations up to date.');
// Set locale_test_translate module to have a local translation available.
$status = locale_translation_get_status();
$status['locale_test_translate']['de']->type = 'local';
\Drupal::keyValue('locale.translation_status')->set('locale_test_translate', $status['locale_test_translate']);
// Check if updates are available for German.
$this->drupalGet('admin/reports/status');
$this->assertSession()->pageTextContains('Translation update status');
$this->assertSession()->pageTextContains("Updates available for: German. See the Available translation updates page for more information.");
$this->assertSession()->linkByHrefExists(Url::fromRoute('locale.translate_status')->toString());
$this->drupalGet('admin/reports/translations');
$this->assertSession()->pageTextContains('Updates for: Locale test translate');
// Set locale_test_translate module to have a dev release and no
// translation found.
$status = locale_translation_get_status();
$status['locale_test_translate']['de']->version = '1.3-dev';
$status['locale_test_translate']['de']->type = '';
\Drupal::keyValue('locale.translation_status')->set('locale_test_translate', $status['locale_test_translate']);
// Check if no updates were found.
$this->drupalGet('admin/reports/status');
$this->assertSession()->pageTextContains('Translation update status');
$this->assertSession()->pageTextContains("Missing translations for: German. See the Available translation updates page for more information.");
$this->assertSession()->linkByHrefExists(Url::fromRoute('locale.translate_status')->toString());
$this->drupalGet('admin/reports/translations');
$this->assertSession()->pageTextContains('Missing translations for one project');
$this->assertSession()->pageTextContains('Locale test translate (1.3-dev). File not found at core/modules/locale/tests/test.de.po');
// Override Drupal core translation status as 'no translations found'.
$status = locale_translation_get_status();
$status['drupal']['de']->type = '';
$status['drupal']['de']->timestamp = 0;
$status['drupal']['de']->version = '8.1.1';
\Drupal::keyValue('locale.translation_status')->set('drupal', $status['drupal']);
// Check if Drupal core is not translated.
$this->drupalGet('admin/reports/translations');
$this->assertSession()->pageTextContains('Missing translations for 2 projects');
$this->assertSession()->pageTextContains('Drupal core (8.1.1).');
// Override Drupal core translation status as 'translations available'.
$status = locale_translation_get_status();
$status['drupal']['de']->type = 'local';
$status['drupal']['de']->files['local']->timestamp = \Drupal::time()->getRequestTime();
$status['drupal']['de']->files['local']->info['version'] = '8.1.1';
\Drupal::keyValue('locale.translation_status')->set('drupal', $status['drupal']);
// Check if translations are available for Drupal core.
$this->drupalGet('admin/reports/translations');
$this->assertSession()->pageTextContains('Updates for: Drupal core');
$this->assertSession()->pageTextContains('Drupal core (' . $this->container->get('date.formatter')->format(\Drupal::time()->getRequestTime(), 'html_date') . ')');
$this->assertSession()->buttonExists('Update translations');
}
}

View File

@@ -0,0 +1,487 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\Core\Database\Database;
use Drupal\Core\Language\LanguageInterface;
// cspell:ignore extraday lundi
/**
* Tests for updating the interface translations of projects.
*
* @group locale
* @group #slow
*/
class LocaleUpdateTest extends LocaleUpdateBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$module_handler = \Drupal::moduleHandler();
$module_handler->loadInclude('locale', 'inc', 'locale.compare');
$module_handler->loadInclude('locale', 'inc', 'locale.fetch');
$admin_user = $this->drupalCreateUser([
'administer modules',
'administer site configuration',
'administer languages',
'access administration pages',
'translate interface',
]);
$this->drupalLogin($admin_user);
// We use German as test language. This language must match the translation
// file that come with the locale_test module (test.de.po) and can therefore
// not be chosen randomly.
$this->addLanguage('de');
}
/**
* Checks if local or remote translation sources are detected.
*
* The translation status process by default checks the status of the
* installed projects. For testing purpose a predefined set of modules with
* fixed file names and release versions is used. This custom project
* definition is applied using a hook_locale_translation_projects_alter
* implementation in the locale_test module.
*
* This test generates a set of local and remote translation files in their
* respective local and remote translation directory. The test checks whether
* the most recent files are selected in the different check scenarios: check
* for local files only, check for both local and remote files.
*/
public function testUpdateCheckStatus(): void {
// Case when contributed modules are absent.
$this->drupalGet('admin/reports/translations');
$this->assertSession()->pageTextContains('Missing translations for one project');
$config = $this->config('locale.settings');
// Set a flag to let the locale_test module replace the project data with a
// set of test projects.
\Drupal::state()->set('locale.test_projects_alter', TRUE);
// Create local and remote translations files.
$this->setTranslationFiles();
$config->set('translation.default_filename', '%project-%version.%language._po')->save();
// Set the test conditions.
$edit = [
'use_source' => LOCALE_TRANSLATION_USE_SOURCE_LOCAL,
];
$this->drupalGet('admin/config/regional/translate/settings');
$this->submitForm($edit, 'Save configuration');
// Get status of translation sources at local file system.
$this->drupalGet('admin/reports/translations/check');
$result = locale_translation_get_status();
$this->assertEquals(LOCALE_TRANSLATION_LOCAL, $result['contrib_module_one']['de']->type, 'Translation of contrib_module_one found');
$this->assertEquals($this->timestampOld, $result['contrib_module_one']['de']->timestamp, 'Translation timestamp found');
$this->assertEquals(LOCALE_TRANSLATION_LOCAL, $result['contrib_module_two']['de']->type, 'Translation of contrib_module_two found');
$this->assertEquals($this->timestampNew, $result['contrib_module_two']['de']->timestamp, 'Translation timestamp found');
$this->assertEquals(LOCALE_TRANSLATION_LOCAL, $result['locale_test']['de']->type, 'Translation of locale_test found');
$this->assertEquals(LOCALE_TRANSLATION_LOCAL, $result['custom_module_one']['de']->type, 'Translation of custom_module_one found');
// Set the test conditions.
$edit = [
'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL,
];
$this->drupalGet('admin/config/regional/translate/settings');
$this->submitForm($edit, 'Save configuration');
// Get status of translation sources at both local and remote locations.
$this->drupalGet('admin/reports/translations/check');
$result = locale_translation_get_status();
$this->assertEquals(LOCALE_TRANSLATION_REMOTE, $result['contrib_module_one']['de']->type, 'Translation of contrib_module_one found');
$this->assertEquals($this->timestampNew, $result['contrib_module_one']['de']->timestamp, 'Translation timestamp found');
$this->assertEquals(LOCALE_TRANSLATION_LOCAL, $result['contrib_module_two']['de']->type, 'Translation of contrib_module_two found');
$this->assertEquals($this->timestampNew, $result['contrib_module_two']['de']->timestamp, 'Translation timestamp found');
$this->assertEquals(LOCALE_TRANSLATION_LOCAL, $result['contrib_module_three']['de']->type, 'Translation of contrib_module_three found');
$this->assertEquals($this->timestampOld, $result['contrib_module_three']['de']->timestamp, 'Translation timestamp found');
$this->assertEquals(LOCALE_TRANSLATION_LOCAL, $result['locale_test']['de']->type, 'Translation of locale_test found');
$this->assertEquals(LOCALE_TRANSLATION_LOCAL, $result['custom_module_one']['de']->type, 'Translation of custom_module_one found');
}
/**
* Tests translation import from remote sources.
*
* Test conditions:
* - Source: remote and local files
* - Import overwrite: all existing translations
*/
public function testUpdateImportSourceRemote(): void {
$config = $this->config('locale.settings');
// Build the test environment.
$this->setTranslationFiles();
$this->setCurrentTranslations();
$config->set('translation.default_filename', '%project-%version.%language._po');
// Set the update conditions for this test.
$edit = [
'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL,
'overwrite' => LOCALE_TRANSLATION_OVERWRITE_ALL,
];
$this->drupalGet('admin/config/regional/translate/settings');
$this->submitForm($edit, 'Save configuration');
// Get the translation status.
$this->drupalGet('admin/reports/translations/check');
// Check the status on the Available translation status page.
$this->assertSession()->responseContains('<label for="edit-langcodes-de" class="visually-hidden">Update German</label>');
$this->assertSession()->pageTextContains('Updates for: Contributed module one, Contributed module two, Custom module one, Locale test');
/** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
$date_formatter = $this->container->get('date.formatter');
$this->assertSession()->pageTextContains('Contributed module one (' . $date_formatter->format($this->timestampNew, 'html_date') . ')');
$this->assertSession()->pageTextContains('Contributed module two (' . $date_formatter->format($this->timestampNew, 'html_date') . ')');
// Execute the translation update.
$this->drupalGet('admin/reports/translations');
$this->submitForm([], 'Update translations');
// Check if the translation has been updated, using the status cache.
$status = locale_translation_get_status();
$this->assertEquals(LOCALE_TRANSLATION_CURRENT, $status['contrib_module_one']['de']->type, 'Translation of contrib_module_one found');
$this->assertEquals(LOCALE_TRANSLATION_CURRENT, $status['contrib_module_two']['de']->type, 'Translation of contrib_module_two found');
$this->assertEquals(LOCALE_TRANSLATION_CURRENT, $status['contrib_module_three']['de']->type, 'Translation of contrib_module_three found');
// Check the new translation status.
// The static cache needs to be flushed first to get the most recent data
// from the database. The function was called earlier during this test.
drupal_static_reset('locale_translation_get_file_history');
$history = locale_translation_get_file_history();
// Verify that the translation of contrib_module_one is imported and
// updated.
$this->assertGreaterThanOrEqual($this->timestampNow, $history['contrib_module_one']['de']->timestamp);
$this->assertGreaterThanOrEqual($this->timestampNow, $history['contrib_module_one']['de']->last_checked);
$this->assertEquals($this->timestampNew, $history['contrib_module_two']['de']->timestamp, 'Translation of contrib_module_two is imported');
// Verify that the translation of contrib_module_two is updated.
$this->assertGreaterThanOrEqual($this->timestampNow, $history['contrib_module_two']['de']->last_checked);
$this->assertEquals($this->timestampMedium, $history['contrib_module_three']['de']->timestamp, 'Translation of contrib_module_three is not imported');
$this->assertEquals($this->timestampMedium, $history['contrib_module_three']['de']->last_checked, 'Translation of contrib_module_three is not updated');
// Check whether existing translations have (not) been overwritten.
// cSpell:disable
$this->assertEquals('Januar_1', t('January', [], ['langcode' => 'de']), 'Translation of January');
$this->assertEquals('Februar_2', t('February', [], ['langcode' => 'de']), 'Translation of February');
$this->assertEquals('Marz_2', t('March', [], ['langcode' => 'de']), 'Translation of March');
$this->assertEquals('April_2', t('April', [], ['langcode' => 'de']), 'Translation of April');
$this->assertEquals('Mai_customized', t('May', [], ['langcode' => 'de']), 'Translation of May');
$this->assertEquals('Juni', t('June', [], ['langcode' => 'de']), 'Translation of June');
$this->assertEquals('Montag', t('Monday', [], ['langcode' => 'de']), 'Translation of Monday');
// cSpell:enable
}
/**
* Tests translation import from local sources.
*
* Test conditions:
* - Source: local files only
* - Import overwrite: all existing translations
*/
public function testUpdateImportSourceLocal(): void {
$config = $this->config('locale.settings');
// Build the test environment.
$this->setTranslationFiles();
$this->setCurrentTranslations();
$config->set('translation.default_filename', '%project-%version.%language._po');
// Set the update conditions for this test.
$edit = [
'use_source' => LOCALE_TRANSLATION_USE_SOURCE_LOCAL,
'overwrite' => LOCALE_TRANSLATION_OVERWRITE_ALL,
];
$this->drupalGet('admin/config/regional/translate/settings');
$this->submitForm($edit, 'Save configuration');
// Execute the translation update.
$this->drupalGet('admin/reports/translations/check');
$this->drupalGet('admin/reports/translations');
$this->submitForm([], 'Update translations');
// Check if the translation has been updated, using the status cache.
$status = locale_translation_get_status();
$this->assertEquals(LOCALE_TRANSLATION_CURRENT, $status['contrib_module_one']['de']->type, 'Translation of contrib_module_one found');
$this->assertEquals(LOCALE_TRANSLATION_CURRENT, $status['contrib_module_two']['de']->type, 'Translation of contrib_module_two found');
$this->assertEquals(LOCALE_TRANSLATION_CURRENT, $status['contrib_module_three']['de']->type, 'Translation of contrib_module_three found');
// Check the new translation status.
// The static cache needs to be flushed first to get the most recent data
// from the database. The function was called earlier during this test.
drupal_static_reset('locale_translation_get_file_history');
$history = locale_translation_get_file_history();
// Verify that the translation of contrib_module_one is imported.
$this->assertGreaterThanOrEqual($this->timestampMedium, $history['contrib_module_one']['de']->timestamp);
$this->assertEquals($this->timestampMedium, $history['contrib_module_one']['de']->last_checked, 'Translation of contrib_module_one is updated');
$this->assertEquals($this->timestampNew, $history['contrib_module_two']['de']->timestamp, 'Translation of contrib_module_two is imported');
// Verify that the translation of contrib_module_two is updated.
$this->assertGreaterThanOrEqual($this->timestampNow, $history['contrib_module_two']['de']->last_checked);
$this->assertEquals($this->timestampMedium, $history['contrib_module_three']['de']->timestamp, 'Translation of contrib_module_three is not imported');
$this->assertEquals($this->timestampMedium, $history['contrib_module_three']['de']->last_checked, 'Translation of contrib_module_three is not updated');
// Check whether existing translations have (not) been overwritten.
// cSpell:disable
$this->assertEquals('Januar_customized', t('January', [], ['langcode' => 'de']), 'Translation of January');
$this->assertEquals('Februar_2', t('February', [], ['langcode' => 'de']), 'Translation of February');
$this->assertEquals('Marz_2', t('March', [], ['langcode' => 'de']), 'Translation of March');
$this->assertEquals('April_2', t('April', [], ['langcode' => 'de']), 'Translation of April');
$this->assertEquals('Mai_customized', t('May', [], ['langcode' => 'de']), 'Translation of May');
$this->assertEquals('Juni', t('June', [], ['langcode' => 'de']), 'Translation of June');
$this->assertEquals('Montag', t('Monday', [], ['langcode' => 'de']), 'Translation of Monday');
// cSpell:enable
}
/**
* Tests translation import and only overwrite non-customized translations.
*
* Test conditions:
* - Source: remote and local files
* - Import overwrite: only overwrite non-customized translations
*/
public function testUpdateImportModeNonCustomized(): void {
$config = $this->config('locale.settings');
// Build the test environment.
$this->setTranslationFiles();
$this->setCurrentTranslations();
$config->set('translation.default_filename', '%project-%version.%language._po');
// Set the test conditions.
$edit = [
'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL,
'overwrite' => LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED,
];
$this->drupalGet('admin/config/regional/translate/settings');
$this->submitForm($edit, 'Save configuration');
// Execute translation update.
$this->drupalGet('admin/reports/translations/check');
$this->drupalGet('admin/reports/translations');
$this->submitForm([], 'Update translations');
// Check whether existing translations have (not) been overwritten.
// cSpell:disable
$this->assertEquals('Januar_customized', t('January', [], ['langcode' => 'de']), 'Translation of January');
$this->assertEquals('Februar_customized', t('February', [], ['langcode' => 'de']), 'Translation of February');
$this->assertEquals('Marz_2', t('March', [], ['langcode' => 'de']), 'Translation of March');
$this->assertEquals('April_2', t('April', [], ['langcode' => 'de']), 'Translation of April');
$this->assertEquals('Mai_customized', t('May', [], ['langcode' => 'de']), 'Translation of May');
$this->assertEquals('Juni', t('June', [], ['langcode' => 'de']), 'Translation of June');
$this->assertEquals('Montag', t('Monday', [], ['langcode' => 'de']), 'Translation of Monday');
// cSpell:enable
}
/**
* Tests translation import and don't overwrite any translation.
*
* Test conditions:
* - Source: remote and local files
* - Import overwrite: don't overwrite any existing translation
*/
public function testUpdateImportModeNone(): void {
$config = $this->config('locale.settings');
// Build the test environment.
$this->setTranslationFiles();
$this->setCurrentTranslations();
$config->set('translation.default_filename', '%project-%version.%language._po');
// Set the test conditions.
$edit = [
'use_source' => LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL,
'overwrite' => LOCALE_TRANSLATION_OVERWRITE_NONE,
];
$this->drupalGet('admin/config/regional/translate/settings');
$this->submitForm($edit, 'Save configuration');
// Execute translation update.
$this->drupalGet('admin/reports/translations/check');
$this->drupalGet('admin/reports/translations');
$this->submitForm([], 'Update translations');
// Check whether existing translations have (not) been overwritten.
// cSpell:disable
$this->assertTranslation('January', 'Januar_customized', 'de');
$this->assertTranslation('February', 'Februar_customized', 'de');
$this->assertTranslation('March', 'Marz', 'de');
$this->assertTranslation('April', 'April_2', 'de');
$this->assertTranslation('May', 'Mai_customized', 'de');
$this->assertTranslation('June', 'Juni', 'de');
$this->assertTranslation('Monday', 'Montag', 'de');
// cSpell:enable
}
/**
* Tests automatic translation import when a module is enabled.
*/
public function testEnableUninstallModule(): void {
// Make the hidden test modules look like a normal custom module.
\Drupal::state()->set('locale.test_system_info_alter', TRUE);
// Check if there is no translation yet.
$this->assertTranslation('Tuesday', '', 'de');
// Enable a module.
$edit = [
'modules[locale_test_translate][enable]' => 'locale_test_translate',
];
$this->drupalGet('admin/modules');
$this->submitForm($edit, 'Install');
// Check if translations have been imported.
$this->assertSession()->pageTextContains("One translation file imported. 7 translations were added, 0 translations were updated and 0 translations were removed.");
// cSpell:disable-next-line
$this->assertTranslation('Tuesday', 'Dienstag', 'de');
$edit = [
'uninstall[locale_test_translate]' => 1,
];
$this->drupalGet('admin/modules/uninstall');
$this->submitForm($edit, 'Uninstall');
$this->submitForm([], 'Uninstall');
// Check if the file data is removed from the database.
$history = locale_translation_get_file_history();
$this->assertFalse(isset($history['locale_test_translate']), 'Project removed from the file history');
$projects = locale_translation_get_projects();
$this->assertFalse(isset($projects['locale_test_translate']), 'Project removed from the project list');
}
/**
* Tests automatic translation import when a language is added.
*
* When a language is added, the system will check for translations files of
* enabled modules and will import them. When a language is removed the system
* will remove all translations of that language from the database.
*/
public function testEnableLanguage(): void {
// Make the hidden test modules look like a normal custom module.
\Drupal::state()->set('locale.test_system_info_alter', TRUE);
// Enable a module.
$edit = [
'modules[locale_test_translate][enable]' => 'locale_test_translate',
];
$this->drupalGet('admin/modules');
$this->submitForm($edit, 'Install');
// Check if there is no Dutch translation yet.
$this->assertTranslation('Extraday', '', 'nl');
// cSpell:disable-next-line
$this->assertTranslation('Tuesday', 'Dienstag', 'de');
// Add a language.
$edit = [
'predefined_langcode' => 'nl',
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add language');
// Check if the right number of translations are added.
$this->assertSession()->pageTextContains("One translation file imported. 8 translations were added, 0 translations were updated and 0 translations were removed.");
// cSpell:disable-next-line
$this->assertTranslation('Extra day', 'extra dag', 'nl');
// Check if the language data is added to the database.
$connection = Database::getConnection();
$result = $connection->select('locale_file', 'lf')
->fields('lf', ['project'])
->condition('langcode', 'nl')
->execute()
->fetchField();
$this->assertNotEmpty($result, 'Files added to file history');
// Remove a language.
$this->drupalGet('admin/config/regional/language/delete/nl');
$this->submitForm([], 'Delete');
// Check if the language data is removed from the database.
$result = $connection->select('locale_file', 'lf')
->fields('lf', ['project'])
->condition('langcode', 'nl')
->execute()
->fetchField();
$this->assertFalse($result, 'Files removed from file history');
// Check that the Dutch translation is gone.
$this->assertTranslation('Extra day', '', 'nl');
// cSpell:disable-next-line
$this->assertTranslation('Tuesday', 'Dienstag', 'de');
}
/**
* Tests automatic translation import when a custom language is added.
*/
public function testEnableCustomLanguage(): void {
// Make the hidden test modules look like a normal custom module.
\Drupal::state()->set('locale.test_system_info_alter', TRUE);
// Enable a module.
$edit = [
'modules[locale_test_translate][enable]' => 'locale_test_translate',
];
$this->drupalGet('admin/modules');
$this->submitForm($edit, 'Install');
// Create a custom language with language code 'xx' and a random
// name.
$langcode = 'xx';
$name = $this->randomMachineName(16);
$edit = [
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add custom language');
// Ensure the translation file is automatically imported when the language
// was added.
$this->assertSession()->pageTextContains('One translation file imported.');
$this->assertSession()->pageTextContains('One translation string was skipped because of disallowed or malformed HTML');
// Ensure the strings were successfully imported.
$search = [
'string' => 'lundi',
'langcode' => $langcode,
'translation' => 'translated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextNotContains('No strings available.');
// Ensure the multiline string was imported.
$search = [
'string' => 'Source string for multiline translation',
'langcode' => $langcode,
'translation' => 'all',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains('Multiline translation string to make sure that import works with it.');
// Ensure 'Allowed HTML source string' was imported but the translation for
// 'Another allowed HTML source string' was not because it contains invalid
// HTML.
$search = [
'string' => 'HTML source string',
'langcode' => $langcode,
'translation' => 'all',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextContains('Allowed HTML source string');
$this->assertSession()->pageTextNotContains('Another allowed HTML source string');
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Functional;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Tests Locale update functions.
*
* @group locale
*/
class LocalesLocationAddIndexUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles[] = $this->root . '/core/modules/system/tests/fixtures/update/drupal-10.3.0.filled.standard.php.gz';
}
/**
* Tests locale_update_10300().
*
* @see locale_update_10300
*/
public function testIndex(): void {
$this->assertFalse(\Drupal::database()
->schema()
->indexExists('locales_location', 'type_name'));
// Run updates and test them.
$this->runUpdates();
$this->assertTrue(\Drupal::database()
->schema()
->indexExists('locales_location', 'type_name'));
}
/**
* Tests locale_update_10300().
*
* @see locale_update_10300
*/
public function testExistingIndex(): void {
$spec = [];
$spec['locales_location'] = [
'description' => 'Location information for source strings.',
'fields' => [
'lid' => [
'type' => 'serial',
'not null' => TRUE,
'description' => 'Unique identifier of this location.',
],
'sid' => [
'type' => 'int',
'not null' => TRUE,
'description' => 'Unique identifier of this string.',
],
'type' => [
'type' => 'varchar_ascii',
'length' => 50,
'not null' => TRUE,
'default' => '',
'description' => 'The location type (file, config, path, etc).',
],
'name' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
'description' => 'Type dependent location information (file name, path, etc).',
],
'version' => [
'type' => 'varchar_ascii',
'length' => 20,
'not null' => TRUE,
'default' => 'none',
'description' => 'Version of Drupal where the location was found.',
],
],
'primary key' => ['lid'],
'foreign keys' => [
'locales_source' => [
'table' => 'locales_source',
'columns' => ['sid' => 'lid'],
],
],
'indexes' => [
'string_type' => ['sid', 'type'],
'type_name' => ['type', 'name'],
],
];
\Drupal::database()->schema()->addIndex('locales_location', 'type_name', ['type', 'name', 'sid'], $spec['locales_location']);
// Run updates and test them.
$this->runUpdates();
// Ensure the update runs successfully even if an index existed prior to
// the update.
$schema = \Drupal::database()->schema();
$this->assertTrue($schema->indexExists('locales_location', 'type_name'));
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests building the translatable project information.
*
* @group locale
*/
class LocaleBuildTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'locale',
'locale_test',
'system',
];
/**
* Checks if a list of translatable projects gets built.
*/
public function testBuildProjects(): void {
$this->container->get('module_handler')->loadInclude('locale', 'compare.inc');
/** @var \Drupal\Core\Extension\ExtensionList $module_list */
$module_list = \Drupal::service('extension.list.module');
// Make the test modules look like a normal custom module. I.e. make the
// modules not hidden. locale_test_system_info_alter() modifies the project
// info of the locale_test and locale_test_translate modules.
\Drupal::state()->set('locale.test_system_info_alter', TRUE);
// Confirm the project name and core value before the module is altered.
$projects = locale_translation_build_projects();
$this->assertSame('locale_test', $projects['locale_test']->name);
$this->assertSame('all', $projects['locale_test']->core);
$projects['locale_test']->langcode = 'de';
$this->assertSame('/all/locale_test/locale_test-1.2.de.po', locale_translation_build_server_pattern($projects['locale_test'], '/%core/%project/%project-%version.%language.po'));
// Alter both the name and core value of the project.
\Drupal::state()->set('locale.test_system_info_alter_name_core', TRUE);
drupal_static_reset('locale_translation_project_list');
$module_list->reset();
// Confirm the name and core value are changed in $module->info.
$module = $module_list->get('locale_test');
$this->assertSame('locale_test_alter', $module->info['name']);
$this->assertSame('8.6.7', $module->info['core']);
$this->assertSame('locale_test', $module->getName());
// Confirm the name and core value are not changed in the project.
$projects = locale_translation_build_projects();
$this->assertSame('locale_test', $projects['locale_test']->name);
$this->assertSame('all', $projects['locale_test']->core);
$projects['locale_test']->langcode = 'de';
$this->assertSame('/all/locale_test/locale_test-1.2.de.po', locale_translation_build_server_pattern($projects['locale_test'], '/%core/%project/%project-%version.%language.po'));
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Kernel;
use Drupal\block\Entity\Block;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests that the locale config manager operates correctly.
*
* @group locale
*/
class LocaleConfigManagerTest extends KernelTestBase {
/**
* A list of modules to install for this test.
*
* @var array
*/
protected static $modules = [
'system',
'language',
'locale',
'locale_test',
'block',
];
/**
* This test creates simple config on the fly breaking schema checking.
*
* @var bool
*/
protected $strictConfigSchema = FALSE;
/**
* Tests hasTranslation().
*/
public function testHasTranslation(): void {
$this->installSchema('locale', ['locales_location', 'locales_source', 'locales_target']);
$this->installConfig(['locale_test']);
$locale_config_manager = \Drupal::service('locale.config_manager');
$language = ConfigurableLanguage::createFromLangcode('de');
$language->save();
$result = $locale_config_manager->hasTranslation('locale_test.no_translation', $language->getId());
$this->assertFalse($result, 'There is no translation for locale_test.no_translation configuration.');
$result = $locale_config_manager->hasTranslation('locale_test.translation', $language->getId());
$this->assertTrue($result, 'There is a translation for locale_test.translation configuration.');
}
/**
* Tests getStringTranslation().
*/
public function testGetStringTranslation(): void {
$this->installSchema('locale', ['locales_location', 'locales_source', 'locales_target']);
$this->installConfig(['locale_test']);
$locale_config_manager = \Drupal::service('locale.config_manager');
$language = ConfigurableLanguage::createFromLangcode('de');
$language->save();
$translation_before = $locale_config_manager->getStringTranslation('locale_test.no_translation', $language->getId(), 'Test', '');
$this->assertTrue($translation_before->isNew());
$translation_before->setString('translation')->save();
$translation_after = $locale_config_manager->getStringTranslation('locale_test.no_translation', $language->getId(), 'Test', '');
$this->assertFalse($translation_after->isNew());
$translation_after->setString('updated_translation')->save();
}
/**
* Tests getDefaultConfigLangcode().
*/
public function testGetDefaultConfigLangcode(): void {
// Install the Language module's configuration so we can use the
// module_installer service.
$this->installConfig(['language']);
$this->assertNull(\Drupal::service('locale.config_manager')->getDefaultConfigLangcode('locale_test_translate.settings'), 'Before installing a module the locale config manager can not access the shipped configuration.');
\Drupal::service('module_installer')->install(['locale_test_translate']);
$this->assertEquals('en', \Drupal::service('locale.config_manager')->getDefaultConfigLangcode('locale_test_translate.settings'), 'After installing a module the locale config manager can get the shipped configuration langcode.');
$simple_config = \Drupal::configFactory()->getEditable('locale_test_translate.simple_config_extra');
$simple_config->set('foo', 'bar')->save();
$this->assertNull(\Drupal::service('locale.config_manager')->getDefaultConfigLangcode($simple_config->getName()), 'Simple config created through the API is not treated as shipped configuration.');
$block = Block::create([
'id' => 'test_default_config',
'theme' => 'stark',
'status' => TRUE,
'region' => 'content',
'plugin' => 'local_tasks_block',
'settings' => [
'id' => 'local_tasks_block',
'label' => $this->randomMachineName(),
'provider' => 'core',
'label_display' => FALSE,
'primary' => TRUE,
'secondary' => TRUE,
],
]);
$block->save();
// Install the theme after creating the block as installing the theme will
// install the block provided by the locale_test module.
\Drupal::service('theme_installer')->install(['stark']);
// The test_default_config block provided by the locale_test module will not
// be installed because a block with the same ID already exists.
$this->installConfig(['locale_test']);
$this->assertNull(\Drupal::service('locale.config_manager')->getDefaultConfigLangcode('block.block.test_default_config'), 'The block.block.test_default_config is not shipped configuration.');
// Delete the block so we can install the one provided by the locale_test
// module.
$block->delete();
$this->installConfig(['locale_test']);
$this->assertEquals('en', \Drupal::service('locale.config_manager')->getDefaultConfigLangcode('block.block.test_default_config'), 'The block.block.test_default_config is shipped configuration.');
// Test the special case for configurable_language config entities.
$fr_language = ConfigurableLanguage::createFromLangcode('fr');
$fr_language->save();
$this->assertEquals('en', \Drupal::service('locale.config_manager')->getDefaultConfigLangcode('language.entity.fr'), 'The language.entity.fr is treated as shipped configuration because it is a configurable_language config entity and in the standard language list.');
$custom_language = ConfigurableLanguage::createFromLangcode('custom');
$custom_language->save();
$this->assertNull(\Drupal::service('locale.config_manager')->getDefaultConfigLangcode('language.entity.custom'), 'The language.entity.custom is not shipped configuration because it is not in the standard language list.');
}
}

View File

@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Kernel;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Language\Language;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests default configuration handling with a foreign default language.
*
* @group locale
*/
class LocaleConfigSubscriberForeignTest extends LocaleConfigSubscriberTest {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
parent::register($container);
$language = Language::$defaultValues;
$language['id'] = 'hu';
$language['name'] = 'Hungarian';
$container->setParameter('language.default_values', $language);
}
/**
* {@inheritdoc}
*/
protected function setUpLanguages() {
parent::setUpLanguages();
ConfigurableLanguage::createFromLangcode('hu')->save();
}
/**
* {@inheritdoc}
*/
protected function setUpLocale() {
parent::setUpLocale();
$this->setUpTranslation('locale_test.translation', 'test', 'English test', 'Hungarian test', 'hu', TRUE);
}
/**
* Tests that the language of default configuration was updated.
*/
public function testDefaultConfigLanguage(): void {
$this->assertEquals('hu', $this->configFactory->getEditable('locale_test.no_translation')->get('langcode'));
$this->assertEquals('hu', $this->configFactory->getEditable('locale_test.translation')->get('langcode'));
$this->assertEquals('Hungarian test', $this->configFactory->getEditable('locale_test.translation')->get('test'));
}
/**
* Tests creating translations of shipped configuration.
*/
public function testCreateActiveTranslation(): void {
$config_name = 'locale_test.no_translation';
$this->saveLanguageActive($config_name, 'test', 'Test (Hungarian)', 'hu');
$this->assertTranslation($config_name, 'Test (Hungarian)', 'hu');
}
/**
* Tests importing community translations of shipped configuration.
*/
public function testLocaleCreateActiveTranslation(): void {
$config_name = 'locale_test.no_translation';
$this->saveLocaleTranslationData($config_name, 'test', 'Test', 'Test (Hungarian)', 'hu', TRUE);
$this->assertTranslation($config_name, 'Test (Hungarian)', 'hu', FALSE);
}
/**
* Tests updating translations of shipped configuration.
*/
public function testUpdateActiveTranslation(): void {
$config_name = 'locale_test.translation';
$this->saveLanguageActive($config_name, 'test', 'Updated Hungarian test', 'hu');
$this->assertTranslation($config_name, 'Updated Hungarian test', 'hu');
}
/**
* Tests updating community translations of shipped configuration.
*/
public function testLocaleUpdateActiveTranslation(): void {
$config_name = 'locale_test.translation';
$this->saveLocaleTranslationData($config_name, 'test', 'English test', 'Updated Hungarian test', 'hu', TRUE);
$this->assertTranslation($config_name, 'Updated Hungarian test', 'hu', FALSE);
}
/**
* Tests deleting a translation override.
*/
public function testDeleteTranslation(): void {
$config_name = 'locale_test.translation';
$this->deleteLanguageOverride($config_name, 'test', 'English test', 'de');
// The German translation in this case will be forced to the Hungarian
// source so its not overwritten with locale data later.
$this->assertTranslation($config_name, 'Hungarian test', 'de');
}
/**
* Tests deleting translations of shipped configuration.
*/
public function testDeleteActiveTranslation(): void {
$config_name = 'locale_test.translation';
$this->configFactory->getEditable($config_name)->delete();
// Deleting active configuration should not change the locale translation.
$this->assertTranslation($config_name, 'Hungarian test', 'hu', FALSE);
}
/**
* Tests deleting community translations of shipped configuration.
*/
public function testLocaleDeleteActiveTranslation(): void {
$config_name = 'locale_test.translation';
$this->deleteLocaleTranslationData($config_name, 'test', 'English test', 'hu');
// Deleting the locale translation should not change active config.
$this->assertEquals('Hungarian test', $this->configFactory->getEditable($config_name)->get('test'));
}
/**
* Tests that adding English creates a translation override.
*/
public function testEnglish(): void {
$config_name = 'locale_test.translation';
ConfigurableLanguage::createFromLangcode('en')->save();
// Adding a language on the UI would normally call updateConfigTranslations.
$this->localeConfigManager->updateConfigTranslations([$config_name], ['en']);
$this->assertConfigOverride($config_name, 'test', 'English test', 'en');
$this->configFactory->getEditable('locale.settings')->set('translate_english', TRUE)->save();
$this->saveLocaleTranslationData($config_name, 'test', 'English test', 'Updated English test', 'en');
$this->assertTranslation($config_name, 'Updated English test', 'en', FALSE);
$this->saveLanguageOverride($config_name, 'test', 'Updated English', 'en');
$this->assertTranslation($config_name, 'Updated English', 'en');
$this->deleteLocaleTranslationData($config_name, 'test', 'English test', 'en');
$this->assertNoConfigOverride($config_name, 'en');
}
/**
* Saves a language override.
*
* This will invoke LocaleConfigSubscriber through the event dispatcher. To
* make sure the configuration was persisted correctly, the configuration
* value is checked. Because LocaleConfigSubscriber temporarily disables the
* override state of the configuration factory we check that the correct value
* is restored afterwards.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $value
* The configuration value to save.
* @param string $langcode
* The language code.
*/
protected function saveLanguageActive($config_name, $key, $value, $langcode) {
$this
->configFactory
->getEditable($config_name)
->set($key, $value)
->save();
$this->assertActiveConfig($config_name, $key, $value, $langcode);
}
}

View File

@@ -0,0 +1,497 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\locale\StringInterface;
use Drupal\locale\TranslationString;
/**
* Tests that shipped configuration translations are updated correctly.
*
* @group locale
*/
class LocaleConfigSubscriberTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['language', 'locale', 'system', 'locale_test'];
/**
* The configurable language manager used in this test.
*
* @var \Drupal\language\ConfigurableLanguageManagerInterface
*/
protected $languageManager;
/**
* The configuration factory used in this test.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The string storage used in this test.
*
* @var \Drupal\locale\StringStorageInterface
*/
protected $stringStorage;
/**
* The locale configuration manager used in this test.
*
* @var \Drupal\locale\LocaleConfigManager
*/
protected $localeConfigManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpDefaultLanguage();
$this->installSchema('locale', ['locales_source', 'locales_target', 'locales_location']);
$this->setupLanguages();
$this->installConfig(['locale_test']);
// Simulate this hook invoked which would happen if in a non-kernel test
// or normal environment.
// @see locale_modules_installed()
// @see locale_system_update()
locale_system_set_config_langcodes();
$langcodes = array_keys(\Drupal::languageManager()->getLanguages());
$locale_config_manager = \Drupal::service('locale.config_manager');
$names = $locale_config_manager->getComponentNames();
$locale_config_manager->updateConfigTranslations($names, $langcodes);
$this->configFactory = $this->container->get('config.factory');
$this->stringStorage = $this->container->get('locale.storage');
$this->localeConfigManager = $this->container->get('locale.config_manager');
$this->languageManager = $this->container->get('language_manager');
$this->setUpLocale();
}
/**
* Sets up default language for this test.
*/
protected function setUpDefaultLanguage() {
// Keep the default English.
}
/**
* Sets up languages needed for this test.
*/
protected function setUpLanguages() {
ConfigurableLanguage::createFromLangcode('de')->save();
}
/**
* Sets up the locale storage strings to be in line with configuration.
*/
protected function setUpLocale() {
// Set up the locale database the same way we have in the config samples.
$this->setUpNoTranslation('locale_test.no_translation', 'test', 'Test', 'de');
$this->setUpTranslation('locale_test.translation', 'test', 'English test', 'German test', 'de');
$this->setUpTranslation('locale_test.translation_multiple', 'test', 'English test', 'German test', 'de');
}
/**
* Tests creating translations of shipped configuration.
*/
public function testCreateTranslation(): void {
$config_name = 'locale_test.no_translation';
$this->saveLanguageOverride($config_name, 'test', 'Test (German)', 'de');
$this->assertTranslation($config_name, 'Test (German)', 'de');
}
/**
* Tests creating translations configuration with multi value settings.
*/
public function testCreateTranslationMultiValue(): void {
$config_name = 'locale_test.translation_multiple';
$this->saveLanguageOverride($config_name, 'test_multiple', ['string' => 'String (German)', 'another_string' => 'Another string (German)'], 'de');
$this->saveLanguageOverride($config_name, 'test_after_multiple', ['string' => 'After string (German)', 'another_string' => 'After another string (German)'], 'de');
$strings = $this->stringStorage->getTranslations([
'type' => 'configuration',
'name' => $config_name,
'language' => 'de',
'translated' => TRUE,
]);
$this->assertCount(5, $strings);
}
/**
* Tests importing community translations of shipped configuration.
*/
public function testLocaleCreateTranslation(): void {
$config_name = 'locale_test.no_translation';
$this->saveLocaleTranslationData($config_name, 'test', 'Test', 'Test (German)', 'de');
$this->assertTranslation($config_name, 'Test (German)', 'de', FALSE);
}
/**
* Tests updating translations of shipped configuration.
*/
public function testUpdateTranslation(): void {
$config_name = 'locale_test.translation';
$this->saveLanguageOverride($config_name, 'test', 'Updated German test', 'de');
$this->assertTranslation($config_name, 'Updated German test', 'de');
}
/**
* Tests updating community translations of shipped configuration.
*/
public function testLocaleUpdateTranslation(): void {
$config_name = 'locale_test.translation';
$this->saveLocaleTranslationData($config_name, 'test', 'English test', 'Updated German test', 'de');
$this->assertTranslation($config_name, 'Updated German test', 'de', FALSE);
}
/**
* Tests deleting translations of shipped configuration.
*/
public function testDeleteTranslation(): void {
$config_name = 'locale_test.translation';
$this->deleteLanguageOverride($config_name, 'test', 'English test', 'de');
// Instead of deleting the translation, we need to keep a translation with
// the source value and mark it as customized to prevent the deletion being
// reverted by importing community translations.
$this->assertTranslation($config_name, 'English test', 'de');
}
/**
* Tests deleting community translations of shipped configuration.
*/
public function testLocaleDeleteTranslation(): void {
$config_name = 'locale_test.translation';
$this->deleteLocaleTranslationData($config_name, 'test', 'English test', 'de');
$this->assertNoTranslation($config_name, 'de');
}
/**
* Sets up a configuration string without a translation.
*
* The actual configuration is already available by installing locale_test
* module, as it is done in LocaleConfigSubscriberTest::setUp(). This sets up
* the necessary source string and verifies that everything is as expected to
* avoid false positives.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $source
* The source string.
* @param string $langcode
* The language code.
*/
protected function setUpNoTranslation($config_name, $key, $source, $langcode) {
$this->localeConfigManager->updateConfigTranslations([$config_name], [$langcode]);
$this->assertNoConfigOverride($config_name, $key);
$this->assertNoTranslation($config_name, $langcode);
}
/**
* Sets up a configuration string with a translation.
*
* The actual configuration is already available by installing locale_test
* module, as it is done in LocaleConfigSubscriberTest::setUp(). This sets up
* the necessary source and translation strings and verifies that everything
* is as expected to avoid false positives.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $source
* The source string.
* @param string $translation
* The translation string.
* @param string $langcode
* The language code.
* @param bool $is_active
* Whether the update will affect the active configuration.
*/
protected function setUpTranslation($config_name, $key, $source, $translation, $langcode, $is_active = FALSE) {
// Create source and translation strings for the configuration value and add
// the configuration name as a location. This would be performed by
// locale_translate_batch_import() invoking
// LocaleConfigManager::updateConfigTranslations() normally.
$this->localeConfigManager->reset();
$this->localeConfigManager
->getStringTranslation($config_name, $langcode, $source, '')
->setString($translation)
->setCustomized(FALSE)
->save();
$this->configFactory->reset($config_name);
$this->localeConfigManager->reset();
$this->localeConfigManager->updateConfigTranslations([$config_name], [$langcode]);
if ($is_active) {
$this->assertActiveConfig($config_name, $key, $translation, $langcode);
}
else {
$this->assertConfigOverride($config_name, $key, $translation, $langcode);
}
$this->assertTranslation($config_name, $translation, $langcode, FALSE);
}
/**
* Saves a language override.
*
* This will invoke LocaleConfigSubscriber through the event dispatcher. To
* make sure the configuration was persisted correctly, the configuration
* value is checked. Because LocaleConfigSubscriber temporarily disables the
* override state of the configuration factory we check that the correct value
* is restored afterwards.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string|array $value
* The configuration value to save.
* @param string $langcode
* The language code.
*/
protected function saveLanguageOverride($config_name, $key, $value, $langcode) {
$translation_override = $this->languageManager
->getLanguageConfigOverride($langcode, $config_name);
$translation_override
->set($key, $value)
->save();
$this->configFactory->reset($config_name);
$this->assertConfigOverride($config_name, $key, $value, $langcode);
}
/**
* Saves translation data from locale module.
*
* This will invoke LocaleConfigSubscriber through the event dispatcher. To
* make sure the configuration was persisted correctly, the configuration
* value is checked. Because LocaleConfigSubscriber temporarily disables the
* override state of the configuration factory we check that the correct value
* is restored afterwards.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $source
* The source string.
* @param string $translation
* The translation string to save.
* @param string $langcode
* The language code.
* @param bool $is_active
* Whether the update will affect the active configuration.
*/
protected function saveLocaleTranslationData($config_name, $key, $source, $translation, $langcode, $is_active = FALSE) {
$this->localeConfigManager->reset();
$this->localeConfigManager
->getStringTranslation($config_name, $langcode, $source, '')
->setString($translation)
->save();
$this->localeConfigManager->reset();
$this->localeConfigManager->updateConfigTranslations([$config_name], [$langcode]);
$this->configFactory->reset($config_name);
if ($is_active) {
$this->assertActiveConfig($config_name, $key, $translation, $langcode);
}
else {
$this->assertConfigOverride($config_name, $key, $translation, $langcode);
}
}
/**
* Deletes a language override.
*
* This will invoke LocaleConfigSubscriber through the event dispatcher. To
* make sure the configuration was persisted correctly, the configuration
* value is checked. Because LocaleConfigSubscriber temporarily disables the
* override state of the configuration factory we check that the correct value
* is restored afterwards.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $source_value
* The source configuration value to verify the correct value is returned
* from the configuration factory after the deletion.
* @param string $langcode
* The language code.
*/
protected function deleteLanguageOverride($config_name, $key, $source_value, $langcode) {
$translation_override = $this->languageManager
->getLanguageConfigOverride($langcode, $config_name);
$translation_override
->clear($key)
->save();
$this->configFactory->reset($config_name);
$this->assertNoConfigOverride($config_name, $key);
}
/**
* Deletes translation data from locale module.
*
* This will invoke LocaleConfigSubscriber through the event dispatcher. To
* make sure the configuration was persisted correctly, the configuration
* value is checked. Because LocaleConfigSubscriber temporarily disables the
* override state of the configuration factory we check that the correct value
* is restored afterwards.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $source_value
* The source configuration value to verify the correct value is returned
* from the configuration factory after the deletion.
* @param string $langcode
* The language code.
*/
protected function deleteLocaleTranslationData($config_name, $key, $source_value, $langcode) {
$this->localeConfigManager
->getStringTranslation($config_name, $langcode, $source_value, '')
->delete();
$this->localeConfigManager->reset();
$this->localeConfigManager->updateConfigTranslations([$config_name], [$langcode]);
$this->configFactory->reset($config_name);
$this->assertNoConfigOverride($config_name, $key);
}
/**
* Ensures configuration override is not present anymore.
*
* @param string $config_name
* The configuration name.
* @param string $langcode
* The language code.
*
* @internal
*/
protected function assertNoConfigOverride(string $config_name, string $langcode): void {
$config_langcode = $this->configFactory->getEditable($config_name)->get('langcode');
$override = $this->languageManager->getLanguageConfigOverride($langcode, $config_name);
$this->assertNotEquals($langcode, $config_langcode);
$this->assertTrue($override->isNew());
}
/**
* Ensures configuration was saved correctly.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string|array $value
* The configuration value.
* @param string $langcode
* The language code.
*
* @internal
*/
protected function assertConfigOverride(string $config_name, string $key, $value, string $langcode): void {
$config_langcode = $this->configFactory->getEditable($config_name)->get('langcode');
$override = $this->languageManager->getLanguageConfigOverride($langcode, $config_name);
$this->assertNotEquals($langcode, $config_langcode);
$this->assertEquals($value, $override->get($key));
}
/**
* Ensures configuration was saved correctly.
*
* @param string $config_name
* The configuration name.
* @param string $key
* The configuration key.
* @param string $value
* The configuration value.
* @param string $langcode
* The language code.
*
* @internal
*/
protected function assertActiveConfig(string $config_name, string $key, string $value, string $langcode): void {
$config = $this->configFactory->getEditable($config_name);
$this->assertEquals($langcode, $config->get('langcode'));
$this->assertSame($value, $config->get($key));
}
/**
* Ensures no translation exists.
*
* @param string $config_name
* The configuration name.
* @param string $langcode
* The language code.
*
* @internal
*/
protected function assertNoTranslation(string $config_name, string $langcode): void {
$strings = $this->stringStorage->getTranslations([
'type' => 'configuration',
'name' => $config_name,
'language' => $langcode,
'translated' => TRUE,
]);
$this->assertSame([], $strings);
}
/**
* Asserts if a specific translation exists and its customization status.
*
* @param string $config_name
* The configuration name.
* @param string|array $translation
* The translation.
* @param string $langcode
* The language code.
* @param bool $customized
* (optional) Asserts if the translation is customized or not.
*
* @internal
*/
protected function assertTranslation(string $config_name, $translation, string $langcode, bool $customized = TRUE): void {
// Make sure a string exists.
$strings = $this->stringStorage->getTranslations([
'type' => 'configuration',
'name' => $config_name,
'language' => $langcode,
'translated' => TRUE,
]);
$this->assertCount(1, $strings);
$string = reset($strings);
$this->assertInstanceOf(StringInterface::class, $string);
/** @var \Drupal\locale\StringInterface $string */
$this->assertSame($translation, $string->getString());
$this->assertTrue($string->isTranslation());
$this->assertInstanceOf(TranslationString::class, $string);
/** @var \Drupal\locale\TranslationString $string */
// Make sure the string is marked as customized so that it does not get
// overridden when the string translations are updated.
$this->assertEquals($customized, (bool) $string->customized);
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Kernel;
use Drupal\Core\Language\LanguageInterface;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests that the configurable language manager and locale operate correctly.
*
* @group locale
*/
class LocaleConfigurableLanguageManagerTest extends KernelTestBase {
/**
* A list of modules to install for this test.
*
* @var array
*/
protected static $modules = ['language', 'locale'];
public function testGetLanguages(): void {
$this->installSchema('locale', ['locales_source', 'locales_target', 'locales_location']);
$default_language = ConfigurableLanguage::create(['label' => $this->randomMachineName(), 'id' => 'default', 'weight' => 0]);
$default_language->save();
// Set new default language.
\Drupal::service('language.default')->set($default_language);
\Drupal::service('string_translation')->setDefaultLangcode($default_language->getId());
$languages = \Drupal::service('language_manager')->getLanguages(LanguageInterface::STATE_ALL);
$this->assertEquals(['default', 'und', 'zxx'], array_keys($languages));
$configurableLanguage = ConfigurableLanguage::create(['label' => $this->randomMachineName(), 'id' => 'test', 'weight' => 1]);
// Simulate a configuration sync by setting the flag otherwise the locked
// language weights would be updated whilst saving.
// @see \Drupal\language\Entity\ConfigurableLanguage::postSave()
$configurableLanguage->setSyncing(TRUE)->save();
$languages = \Drupal::service('language_manager')->getLanguages(LanguageInterface::STATE_ALL);
$this->assertEquals(['default', 'test', 'und', 'zxx'], array_keys($languages));
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Kernel;
use Drupal\Core\Config\NullStorage;
use Drupal\KernelTests\KernelTestBase;
use Drupal\locale\LocaleDefaultConfigStorage;
/**
* @group locale
*/
class LocaleDefaultConfigStorageTest extends KernelTestBase {
protected static $modules = [
'language',
'locale',
'locale_test',
'locale_test_translate',
];
public function testGetComponentNames(): void {
$storage = new LocaleDefaultConfigStorage(
new NullStorage(),
\Drupal::languageManager(),
'testing',
);
$expected = [
'locale_test.no_translation',
'locale_test.translation',
'locale_test.translation_multiple',
'locale_test_translate.settings',
'block.block.test_default_config',
];
$actual = $storage->getComponentNames(
'module',
[
\Drupal::moduleHandler()->getModule('locale_test'),
\Drupal::moduleHandler()->getModule('locale_test_translate'),
],
);
$this->assertSame($expected, $actual);
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests locale translation safe string handling.
*
* @group locale
*/
class LocaleStringIsSafeTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['locale', 'locale_test'];
/**
* Tests for locale_string_is_safe().
*/
public function testLocaleStringIsSafe(): void {
// Check a translatable string without HTML.
$string = 'Hello world!';
$result = locale_string_is_safe($string);
$this->assertTrue($result);
// Check a translatable string which includes trustable HTML.
$string = 'Hello <strong>world</strong>!';
$result = locale_string_is_safe($string);
$this->assertTrue($result);
// Check an untranslatable string which includes unsafe HTML (according to
// the locale_string_is_safe() function definition).
$string = 'Hello <img src="world.png" alt="world" />!';
$result = locale_string_is_safe($string);
$this->assertFalse($result);
// Check a translatable string which includes a token in an href attribute.
$string = 'Hi <a href="[current-user:url]">user</a>';
$result = locale_string_is_safe($string);
$this->assertTrue($result);
}
/**
* Tests if a translated and tokenized string is properly escaped by Twig.
*
* In each assert* call we add a new line at the expected result to match the
* newline at the end of the template file.
*/
public function testLocalizedTokenizedString(): void {
$tests_to_do = [
1 => [
'original' => 'Go to the <a href="[locale_test:security_test1]">frontpage</a>',
'replaced' => 'Go to the &lt;a href=&quot;javascript:alert(&amp;#039;Hello!&amp;#039;);&quot;&gt;frontpage&lt;/a&gt;',
],
2 => [
'original' => 'Hello <strong>[locale_test:security_test2]</strong>!',
'replaced' => 'Hello &lt;strong&gt;&amp;lt;script&amp;gt;alert(&amp;#039;Hello!&amp;#039;);&amp;lt;/script&amp;gt;&lt;/strong&gt;!',
],
];
foreach ($tests_to_do as $i => $test) {
$original_string = $test['original'];
$rendered_original_string = \Drupal::theme()->render('locale_test_tokenized', ['content' => $original_string]);
// Twig assumes that strings are unsafe so it escapes them, and so the
// original and the rendered version should be different.
$this->assertNotEquals(
$original_string . "\n",
$rendered_original_string,
'Security test ' . $i . ' before translation'
);
// Pass the original string to the t() function to get it marked as safe.
$safe_string = t($original_string);
$rendered_safe_string = \Drupal::theme()->render('locale_test_tokenized', ['content' => $safe_string]);
// t() function always marks the string as safe so it won't be escaped,
// and should be the same as the original.
$this->assertSame($original_string . "\n", (string) $rendered_safe_string, 'Security test ' . $i . ' after translation before token replacement');
// Replace tokens in the safe string to inject it with dangerous content.
// @see locale_test_tokens().
$unsafe_string = \Drupal::token()->replace($safe_string);
$rendered_unsafe_string = \Drupal::theme()->render('locale_test_tokenized', ['content' => $unsafe_string]);
// Token replacement changes the string so it is not marked as safe
// anymore. Check it is escaped the way we expect.
$this->assertEquals($test['replaced'] . "\n", $rendered_unsafe_string, 'Security test ' . $i . ' after translation after token replacement');
}
}
}

View File

@@ -0,0 +1,245 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\locale\StringInterface;
/**
* Tests the locale string storage, string objects and data API.
*
* @group locale
*/
class LocaleStringTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'language',
'locale',
];
/**
* The locale storage.
*
* @var \Drupal\locale\StringStorageInterface
*/
protected $storage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Add a default locale storage for all these tests.
$this->storage = $this->container->get('locale.storage');
// Create two languages: Spanish and German.
foreach (['es', 'de'] as $langcode) {
ConfigurableLanguage::createFromLangcode($langcode)->save();
}
$this->installSchema('locale', [
'locales_location',
'locales_source',
'locales_target',
]);
}
/**
* Tests CRUD API.
*/
public function testStringCrudApi(): void {
// Create source string.
$source = $this->buildSourceString()->save();
$this->assertNotEmpty($source->lid);
// Load strings by lid and source.
$string1 = $this->storage->findString(['lid' => $source->lid]);
$this->assertEquals($source, $string1);
$string2 = $this->storage->findString(['source' => $source->source, 'context' => $source->context]);
$this->assertEquals($source, $string2);
$string3 = $this->storage->findString(['source' => $source->source, 'context' => '']);
$this->assertNull($string3);
// Check version handling and updating.
$this->assertEquals('none', $source->version);
$string = $this->storage->findTranslation(['lid' => $source->lid]);
$this->assertEquals(\Drupal::VERSION, $string->version);
// Create translation and find it by lid and source.
$langcode = 'es';
$translation = $this->createTranslation($source, $langcode);
$this->assertEquals(LOCALE_NOT_CUSTOMIZED, $translation->customized);
$string1 = $this->storage->findTranslation(['language' => $langcode, 'lid' => $source->lid]);
$this->assertEquals($translation->translation, $string1->translation);
$string2 = $this->storage->findTranslation(['language' => $langcode, 'source' => $source->source, 'context' => $source->context]);
$this->assertEquals($translation->translation, $string2->translation);
$translation
->setCustomized()
->save();
$translation = $this->storage->findTranslation(['language' => $langcode, 'lid' => $source->lid]);
$this->assertEquals(LOCALE_CUSTOMIZED, $translation->customized);
// Delete translation.
$translation->delete();
$deleted = $this->storage->findTranslation(['language' => $langcode, 'lid' => $source->lid]);
$this->assertNull($deleted->translation);
// Create some translations and then delete string and all of its
// translations.
$lid = $source->lid;
$this->createAllTranslations($source);
$search = $this->storage->getTranslations(['lid' => $source->lid]);
$this->assertCount(3, $search);
$source->delete();
$string = $this->storage->findString(['lid' => $lid]);
$this->assertNull($string);
$deleted = $search = $this->storage->getTranslations(['lid' => $lid]);
$this->assertEmpty($deleted);
// Tests that locations of different types and arbitrary lengths can be
// added to a source string. Too long locations will be cut off.
$source_string = $this->buildSourceString();
$source_string->addLocation('javascript', $this->randomString(8));
$source_string->addLocation('configuration', $this->randomString(50));
$source_string->addLocation('code', $this->randomString(100));
$source_string->addLocation('path', $location = $this->randomString(300));
$source_string->save();
$rows = $this->container->get('database')->select('locales_location')
->fields('locales_location')
->condition('sid', $source_string->lid)
->execute()
->fetchAllAssoc('type');
$this->assertCount(4, $rows);
$this->assertEquals(substr($location, 0, 255), $rows['path']->name);
}
/**
* Tests Search API loading multiple objects.
*/
public function testStringSearchApi(): void {
$language_count = 3;
// Strings 1 and 2 will have some common prefix.
// Source 1 will have all translations, not customized.
// Source 2 will have all translations, customized.
// Source 3 will have no translations.
$prefix = $this->randomMachineName(100);
$source1 = $this->buildSourceString(['source' => $prefix . $this->randomMachineName(100)])->save();
$source2 = $this->buildSourceString(['source' => $prefix . $this->randomMachineName(100)])->save();
$source3 = $this->buildSourceString()->save();
// Load all source strings.
$strings = $this->storage->getStrings([]);
$this->assertCount(3, $strings);
// Load all source strings matching a given string.
$filter_options['filters'] = ['source' => $prefix];
$strings = $this->storage->getStrings([], $filter_options);
$this->assertCount(2, $strings);
// Not customized translations.
$translate1 = $this->createAllTranslations($source1);
// Customized translations.
$this->createAllTranslations($source2, ['customized' => LOCALE_CUSTOMIZED]);
// Try quick search function with different field combinations.
$langcode = 'es';
$found = $this->storage->findTranslation(['language' => $langcode, 'source' => $source1->source, 'context' => $source1->context]);
$this->assertNotNull($found, 'Translation not found searching by source and context.');
$this->assertNotNull($found->language);
$this->assertNotNull($found->translation);
$this->assertFalse($found->isNew());
$this->assertEquals($translate1[$langcode]->translation, $found->translation);
// Now try a translation not found.
$found = $this->storage->findTranslation(['language' => $langcode, 'source' => $source3->source, 'context' => $source3->context]);
$this->assertNotNull($found);
$this->assertSame($source3->lid, $found->lid);
$this->assertNull($found->translation);
$this->assertTrue($found->isNew());
// Load all translations. For next queries we'll be loading only translated
// strings.
$translations = $this->storage->getTranslations(['translated' => TRUE]);
$this->assertCount(2 * $language_count, $translations);
// Load all customized translations.
$translations = $this->storage->getTranslations(['customized' => LOCALE_CUSTOMIZED, 'translated' => TRUE]);
$this->assertCount($language_count, $translations);
// Load all Spanish customized translations.
$translations = $this->storage->getTranslations(['language' => 'es', 'customized' => LOCALE_CUSTOMIZED, 'translated' => TRUE]);
$this->assertCount(1, $translations);
// Load all source strings without translation (1).
$translations = $this->storage->getStrings(['translated' => FALSE]);
$this->assertCount(1, $translations);
// Load Spanish translations using string filter.
$filter_options['filters'] = ['source' => $prefix];
$translations = $this->storage->getTranslations(['language' => 'es'], $filter_options);
$this->assertCount(2, $translations);
}
/**
* Creates random source string object.
*
* @param array $values
* The values array.
*
* @return \Drupal\locale\StringInterface
* A locale string.
*/
protected function buildSourceString(array $values = []) {
return $this->storage->createString($values += [
'source' => $this->randomMachineName(100),
'context' => $this->randomMachineName(20),
]);
}
/**
* Creates translations for source string and all languages.
*
* @param \Drupal\locale\StringInterface $source
* The source string.
* @param array $values
* The values array.
*
* @return array
* Translation list.
*/
protected function createAllTranslations(StringInterface $source, array $values = []) {
$list = [];
/** @var \Drupal\Core\Language\LanguageManagerInterface $language_manager */
$language_manager = $this->container->get('language_manager');
foreach ($language_manager->getLanguages() as $language) {
$list[$language->getId()] = $this->createTranslation($source, $language->getId(), $values);
}
return $list;
}
/**
* Creates single translation for source string.
*
* @param \Drupal\locale\StringInterface $source
* The source string.
* @param string $langcode
* The language code.
* @param array $values
* The values array.
*
* @return \Drupal\locale\StringInterface
* The translated string object.
*/
protected function createTranslation(StringInterface $source, $langcode, array $values = []) {
return $this->storage->createTranslation($values + [
'lid' => $source->lid,
'language' => $langcode,
'translation' => $this->randomMachineName(100),
])->save();
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests locale translation project handling.
*
* @group locale
*/
class LocaleTranslationProjectsTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['locale', 'locale_test', 'system'];
/**
* The module handler used in this test.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The locale project storage used in this test.
*
* @var \Drupal\locale\LocaleProjectStorageInterface
*/
protected $projectStorage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->moduleHandler = $this->container->get('module_handler');
$this->projectStorage = $this->container->get('locale.project');
\Drupal::state()->set('locale.remove_core_project', TRUE);
}
/**
* Tests locale_translation_clear_cache_projects().
*/
public function testLocaleTranslationClearCacheProjects(): void {
$this->moduleHandler->loadInclude('locale', 'inc', 'locale.translation');
$expected = [];
$this->assertSame($expected, locale_translation_get_projects());
$this->projectStorage->set('foo', []);
$expected['foo'] = new \stdClass();
$this->assertEquals($expected, locale_translation_get_projects());
$this->projectStorage->set('bar', []);
locale_translation_clear_cache_projects();
$expected['bar'] = new \stdClass();
$this->assertEquals($expected, locale_translation_get_projects());
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Kernel;
use Drupal\locale\LocaleTranslation;
use Drupal\KernelTests\KernelTestBase;
/**
* @coversDefaultClass \Drupal\locale\LocaleTranslation
* @group locale
*/
class LocaleTranslationTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'locale',
];
/**
* Tests that \Drupal\locale\LocaleTranslation is serializable.
*/
public function testSerializable(): void {
$translation = $this->container->get('string_translator.locale.lookup');
$this->assertInstanceOf(LocaleTranslation::class, $translation);
// Prove that serialization and deserialization works without errors.
$this->assertNotNull($translation);
$unserialized = unserialize(serialize($translation));
$this->assertInstanceOf(LocaleTranslation::class, $unserialized);
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests for updating the interface translations of projects.
*
* @group locale
*/
class LocaleUpdateTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'locale',
'locale_test',
'system',
];
/**
* Checks if a list of translatable projects gets build.
*/
public function testUpdateProjects(): void {
$this->container->get('module_handler')->loadInclude('locale', 'compare.inc');
// Make the test modules look like a normal custom module. I.e. make the
// modules not hidden. locale_test_system_info_alter() modifies the project
// info of the locale_test and locale_test_translate modules.
\Drupal::state()->set('locale.test_system_info_alter', TRUE);
// Check if interface translation data is collected from hook_info.
$projects = locale_translation_project_list();
$this->assertArrayNotHasKey('locale_test_translate', $projects);
$this->assertEquals('core/modules/locale/test/test.%language.po', $projects['locale_test']['info']['interface translation server pattern']);
$this->assertEquals('locale_test', $projects['locale_test']['name']);
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Kernel\Migrate;
use Drupal\Tests\SchemaCheckTestTrait;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Upgrade variables to locale.settings.yml.
*
* @group migrate_drupal_6
*/
class MigrateLocaleConfigsTest extends MigrateDrupal6TestBase {
use SchemaCheckTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['locale', 'language'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigration('locale_settings');
}
/**
* Tests migration of locale variables to locale.settings.yml.
*/
public function testLocaleSettings(): void {
$config = $this->config('locale.settings');
$this->assertTrue($config->get('cache_strings'));
$this->assertSame('languages', $config->get('javascript.directory'));
$this->assertConfigSchema(\Drupal::service('config.typed'), 'locale.settings', $config->get());
}
}

View File

@@ -0,0 +1,390 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Unit;
use Drupal\Component\Gettext\PoItem;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\locale\LocaleLookup;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* @coversDefaultClass \Drupal\locale\LocaleLookup
* @group locale
*/
class LocaleLookupTest extends UnitTestCase {
/**
* A mocked storage to use when instantiating LocaleTranslation objects.
*
* @var \Drupal\locale\StringStorageInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $storage;
/**
* A mocked cache object.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $cache;
/**
* A mocked lock object.
*
* @var \Drupal\Core\Lock\LockBackendInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $lock;
/**
* A mocked user object built from AccountInterface.
*
* @var \Drupal\Core\Session\AccountInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $user;
/**
* A mocked config factory built with UnitTestCase::getConfigFactoryStub().
*
* @var \Drupal\Core\Config\ConfigFactory|\PHPUnit\Framework\MockObject\MockBuilder
*/
protected $configFactory;
/**
* A mocked language manager built from LanguageManagerInterface.
*
* @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $languageManager;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->storage = $this->createMock('Drupal\locale\StringStorageInterface');
$this->cache = $this->createMock('Drupal\Core\Cache\CacheBackendInterface');
$this->lock = $this->createMock('Drupal\Core\Lock\LockBackendInterface');
$this->lock->expects($this->never())
->method($this->anything());
$this->user = $this->createMock('Drupal\Core\Session\AccountInterface');
$this->user->expects($this->any())
->method('getRoles')
->willReturn(['anonymous']);
$this->configFactory = $this->getConfigFactoryStub(['locale.settings' => ['cache_strings' => FALSE]]);
$this->languageManager = $this->createMock('Drupal\Core\Language\LanguageManagerInterface');
$this->requestStack = new RequestStack();
$container = new ContainerBuilder();
$container->set('current_user', $this->user);
\Drupal::setContainer($container);
}
/**
* Tests locale lookups without fallback.
*
* @covers ::resolveCacheMiss
*/
public function testResolveCacheMissWithoutFallback(): void {
$args = [
'language' => 'en',
'source' => 'test',
'context' => 'irrelevant',
];
$result = (object) [
'translation' => 'test',
];
$this->cache->expects($this->once())
->method('get')
->with('locale:en:irrelevant:anonymous', FALSE);
$this->storage->expects($this->once())
->method('findTranslation')
->with($this->equalTo($args))
->willReturn($result);
$locale_lookup = $this->getMockBuilder('Drupal\locale\LocaleLookup')
->setConstructorArgs(['en', 'irrelevant', $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager, $this->requestStack])
->onlyMethods(['persist'])
->getMock();
$locale_lookup->expects($this->never())
->method('persist');
$this->assertSame('test', $locale_lookup->get('test'));
}
/**
* Tests locale lookups with fallback.
*
* Note that context is irrelevant here. It is not used but it is required.
*
* @covers ::resolveCacheMiss
*
* @dataProvider resolveCacheMissWithFallbackProvider
*/
public function testResolveCacheMissWithFallback($langcode, $string, $context, $expected): void {
// These are fake words!
// cSpell:disable
$translations = [
'en' => [
'test' => 'test',
'fake' => 'fake',
'missing pl' => 'missing pl',
'missing cs' => 'missing cs',
'missing both' => 'missing both',
],
'pl' => [
'test' => 'test po polsku',
'fake' => 'ściema',
'missing cs' => 'zaginiony czech',
],
'cs' => [
'test' => 'test v české',
'fake' => 'falešný',
'missing pl' => 'chybějící pl',
],
];
// cSpell:enable
$this->storage->expects($this->any())
->method('findTranslation')
->willReturnCallback(function ($argument) use ($translations) {
if (isset($translations[$argument['language']][$argument['source']])) {
return (object) ['translation' => $translations[$argument['language']][$argument['source']]];
}
return TRUE;
});
$this->languageManager->expects($this->any())
->method('getFallbackCandidates')
->willReturnCallback(function (array $context = []) {
switch ($context['langcode']) {
case 'pl':
return ['cs', 'en'];
case 'cs':
return ['en'];
default:
return [];
}
});
$this->cache->expects($this->once())
->method('get')
->with('locale:' . $langcode . ':' . $context . ':anonymous', FALSE);
$locale_lookup = new LocaleLookup($langcode, $context, $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager, $this->requestStack);
$this->assertSame($expected, $locale_lookup->get($string));
}
/**
* Provides test data for testResolveCacheMissWithFallback().
*/
public static function resolveCacheMissWithFallbackProvider() {
// cSpell:disable
return [
['cs', 'test', 'irrelevant', 'test v české'],
['cs', 'fake', 'irrelevant', 'falešný'],
['cs', 'missing pl', 'irrelevant', 'chybějící pl'],
['cs', 'missing cs', 'irrelevant', 'missing cs'],
['cs', 'missing both', 'irrelevant', 'missing both'],
// Testing PL with fallback to cs, en.
['pl', 'test', 'irrelevant', 'test po polsku'],
['pl', 'fake', 'irrelevant', 'ściema'],
['pl', 'missing pl', 'irrelevant', 'chybějící pl'],
['pl', 'missing cs', 'irrelevant', 'zaginiony czech'],
['pl', 'missing both', 'irrelevant', 'missing both'],
];
// cSpell:enable
}
/**
* Tests locale lookups with persistent tracking.
*
* @covers ::resolveCacheMiss
*/
public function testResolveCacheMissWithPersist(): void {
$args = [
'language' => 'en',
'source' => 'test',
'context' => 'irrelevant',
];
$result = (object) [
'translation' => 'test',
];
$this->storage->expects($this->once())
->method('findTranslation')
->with($this->equalTo($args))
->willReturn($result);
$this->configFactory = $this->getConfigFactoryStub(['locale.settings' => ['cache_strings' => TRUE]]);
$locale_lookup = $this->getMockBuilder('Drupal\locale\LocaleLookup')
->setConstructorArgs(['en', 'irrelevant', $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager, $this->requestStack])
->onlyMethods(['persist'])
->getMock();
$locale_lookup->expects($this->once())
->method('persist');
$this->assertSame('test', $locale_lookup->get('test'));
}
/**
* Tests locale lookups without a found translation.
*
* @covers ::resolveCacheMiss
*/
public function testResolveCacheMissNoTranslation(): void {
$string = $this->createMock('Drupal\locale\StringInterface');
$string->expects($this->once())
->method('addLocation')
->willReturnSelf();
$this->storage->expects($this->once())
->method('findTranslation')
->willReturn(NULL);
$this->storage->expects($this->once())
->method('createString')
->willReturn($string);
$request = Request::create('/test');
$this->requestStack->push($request);
$locale_lookup = $this->getMockBuilder('Drupal\locale\LocaleLookup')
->setConstructorArgs(['en', 'irrelevant', $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager, $this->requestStack])
->onlyMethods(['persist'])
->getMock();
$locale_lookup->expects($this->never())
->method('persist');
$this->assertTrue($locale_lookup->get('test'));
}
/**
* Tests locale lookups with old plural style of translations.
*
* @param array $translations
* The source with translations.
* @param string $langcode
* The language code of translation string.
* @param string $string
* The string for translation.
* @param bool $is_fix
* The flag about expected fix translation.
*
* @covers ::resolveCacheMiss
* @dataProvider providerFixOldPluralTranslationProvider
*/
public function testFixOldPluralStyleTranslations($translations, $langcode, $string, $is_fix): void {
$this->storage->expects($this->any())
->method('findTranslation')
->willReturnCallback(function ($argument) use ($translations) {
if (isset($translations[$argument['language']][$argument['source']])) {
return (object) ['translation' => $translations[$argument['language']][$argument['source']]];
}
return TRUE;
});
$this->languageManager->expects($this->any())
->method('getFallbackCandidates')
->willReturnCallback(function (array $context = []) {
switch ($context['langcode']) {
case 'by':
return ['ru'];
}
});
$this->cache->expects($this->once())
->method('get')
->with('locale:' . $langcode . '::anonymous', FALSE);
$locale_lookup = new LocaleLookup($langcode, '', $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager, $this->requestStack);
if ($is_fix) {
$this->assertStringNotContainsString('@count[2]', $locale_lookup->get($string));
}
else {
$this->assertStringContainsString('@count[2]', $locale_lookup->get($string));
}
}
/**
* Provides test data for testResolveCacheMissWithFallback().
*/
public static function providerFixOldPluralTranslationProvider() {
$translations = [
'by' => [
'word1' => '@count[2] word-by',
'word2' => implode(PoItem::DELIMITER, ['word-by', '@count[2] word-by']),
],
'ru' => [
'word3' => '@count[2] word-ru',
'word4' => implode(PoItem::DELIMITER, ['word-ru', '@count[2] word-ru']),
],
];
return [
'no-plural' => [$translations, 'by', 'word1', FALSE],
'no-plural from other language' => [$translations, 'by', 'word3', FALSE],
'plural' => [$translations, 'by', 'word2', TRUE],
'plural from other language' => [$translations, 'by', 'word4', TRUE],
];
}
/**
* @covers ::getCid
*
* @dataProvider getCidProvider
*/
public function testGetCid(array $roles, $expected): void {
$this->user = $this->createMock('Drupal\Core\Session\AccountInterface');
$this->user->expects($this->any())
->method('getRoles')
->willReturn($roles);
$container = new ContainerBuilder();
$container->set('current_user', $this->user);
\Drupal::setContainer($container);
$locale_lookup = $this->getMockBuilder('Drupal\locale\LocaleLookup')
->setConstructorArgs(['en', 'irrelevant', $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager, $this->requestStack])
->getMock();
$o = new \ReflectionObject($locale_lookup);
$method = $o->getMethod('getCid');
$cid = $method->invoke($locale_lookup, 'getCid');
$this->assertEquals($expected, $cid);
}
/**
* Provides test data for testGetCid().
*/
public static function getCidProvider() {
return [
[
['a'], 'locale:en:irrelevant:a',
],
[
['a', 'b'], 'locale:en:irrelevant:a:b',
],
[
['b', 'a'], 'locale:en:irrelevant:a:b',
],
];
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Unit;
use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
use Drupal\locale\LocaleProjectStorage;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\locale\LocaleProjectStorage
* @group locale
* @runTestsInSeparateProcesses
*/
class LocaleProjectStorageTest extends UnitTestCase {
/**
* @var \Drupal\locale\LocaleProjectStorage
*/
private LocaleProjectStorage $projectStorage;
/**
* @var \Drupal\Core\KeyValueStore\KeyValueMemoryFactory
*/
private KeyValueMemoryFactory $keyValueMemoryFactory;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->keyValueMemoryFactory = new KeyValueMemoryFactory();
$this->projectStorage = new LocaleProjectStorage($this->keyValueMemoryFactory);
}
/**
* Tests that projects are sorted by weight and key.
*/
public function testSorting(): void {
// There are no projects.
$this->assertSame([], $this->projectStorage->getAll());
// Add project 'b'.
$this->projectStorage->set('b', ['name' => 'b']);
$this->assertSame(['b'], array_keys($this->projectStorage->getAll()));
// Add project 'c' and confirm alphabetical order.
$this->projectStorage->set('c', ['name' => 'c']);
$this->assertSame(['b', 'c'], array_keys($this->projectStorage->getAll()));
// Add project 'a' and confirm 'a' is first.
$this->projectStorage->set('a', ['name' => 'a']);
$this->assertSame(['a', 'b', 'c'], array_keys($this->projectStorage->getAll()));
// Add project 'd' with a negative weight and confirm 'd' is first.
$this->projectStorage->set('d', ['name' => 'd', 'weight' => -1]);
$this->assertSame(['d', 'a', 'b', 'c'], array_keys($this->projectStorage->getAll()));
// Add project 'aa' with a positive weight and confirm 'aa' is last.
$this->projectStorage->set('aa', ['name' => 'aa', 'weight' => 1]);
$this->assertSame(['d', 'a', 'b', 'c', 'aa'], array_keys($this->projectStorage->getAll()));
// Delete project 'a'.
$this->projectStorage->delete('a');
$this->assertSame(['d', 'b', 'c', 'aa'], array_keys($this->projectStorage->getAll()));
// Add project 'e' with a lower negative weight than 'd' and confirm 'e' is
// first.
$this->projectStorage->set('e', ['name' => 'e', 'weight' => -5]);
$this->assertSame(['e', 'd', 'b', 'c', 'aa'], array_keys($this->projectStorage->getAll()));
// Pretend there is a container rebuild by generating a new
// LocaleProjectStorage object with the same data.
$this->projectStorage = new LocaleProjectStorage($this->keyValueMemoryFactory);
$this->projectStorage->set('z', ['name' => 'z']);
$this->assertSame(['e', 'd', 'b', 'c', 'z', 'aa'], array_keys($this->projectStorage->getAll()));
// Now delete all projects.
$this->projectStorage->deleteAll();
$this->assertSame([], $this->projectStorage->getAll());
// Add project 'z' before project 'a' and confirm 'a' is first.
$this->projectStorage->set('z', ['name' => 'z']);
$this->projectStorage->set('a', ['name' => 'a']);
$this->assertSame(['a', 'z'], array_keys($this->projectStorage->getAll()));
}
/**
* Tests deleted projects are not included in the count.
*/
public function testDelete(): void {
$this->projectStorage->set('b', ['name' => 'b']);
$this->assertSame(['name' => 'b'], $this->projectStorage->get('b'));
$this->assertSame(1, $this->projectStorage->countProjects());
$this->projectStorage->delete('b');
$this->assertNull($this->projectStorage->get('b'));
$this->assertSame(0, $this->projectStorage->countProjects());
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Unit;
use Drupal\Core\DependencyInjection\Container;
use Drupal\locale\Locale;
use Drupal\locale\LocaleConfigManager;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\locale\Locale
* @group Cache
*/
class LocaleTest extends UnitTestCase {
/**
* Tests deprecation of config() method.
*
* @covers ::config
* @group legacy
*/
public function testConfig(): void {
$config_manager = $this->prophesize(LocaleConfigManager::class);
$container = $this->prophesize(Container::class);
$container->get('locale.config_manager')
->willReturn($config_manager->reveal());
\Drupal::setContainer($container->reveal());
$this->expectDeprecation('The Drupal\locale\Locale is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3437110');
$this->expectDeprecation('Drupal\locale\Locale::config() is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal::service(\'locale.config_manager\') instead. See https://www.drupal.org/node/3437110');
$this->assertInstanceOf(LocaleConfigManager::class, Locale::config());
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Unit;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\locale\LocaleTranslation;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* @coversDefaultClass \Drupal\locale\LocaleTranslation
* @group locale
*/
class LocaleTranslationTest extends UnitTestCase {
/**
* A mocked storage to use when instantiating LocaleTranslation objects.
*
* @var \PHPUnit\Framework\MockObject\MockObject
*/
protected $storage;
/**
* A mocked lock to use when instantiating LocaleTranslation objects.
*
* @var \Drupal\Core\Lock\LockBackendInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected LockBackendInterface $lock;
/**
* A mocked cache to use when instantiating LocaleTranslation objects.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected CacheBackendInterface $cache;
/**
* A mocked language manager built from LanguageManagerInterface.
*
* @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $languageManager;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->storage = $this->createMock('Drupal\locale\StringStorageInterface');
$this->cache = $this->createMock('Drupal\Core\Cache\CacheBackendInterface');
$this->lock = $this->createMock('Drupal\Core\Lock\LockBackendInterface');
$this->languageManager = $this->createMock('Drupal\Core\Language\LanguageManagerInterface');
$this->requestStack = new RequestStack();
}
/**
* Tests for \Drupal\locale\LocaleTranslation::destruct().
*/
public function testDestruct(): void {
$translation = new LocaleTranslation($this->storage, $this->cache, $this->lock, $this->getConfigFactoryStub(), $this->languageManager, $this->requestStack);
// Prove that destruction works without errors when translations are empty.
$this->assertNull($translation->destruct());
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Unit\Menu;
use Drupal\Tests\Core\Menu\LocalTaskIntegrationTestBase;
/**
* Tests locale local tasks.
*
* @group locale
*/
class LocaleLocalTasksTest extends LocalTaskIntegrationTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->directoryList = [
'locale' => 'core/modules/locale',
];
parent::setUp();
}
/**
* Checks locale listing local tasks.
*
* @dataProvider getLocalePageRoutes
*/
public function testLocalePageLocalTasks($route): void {
$tasks = [
0 => ['locale.translate_page', 'locale.translate_import', 'locale.translate_export', 'locale.settings'],
];
$this->assertLocalTasks($route, $tasks);
}
/**
* Provides a list of routes to test.
*/
public static function getLocalePageRoutes() {
return [
['locale.translate_page'],
['locale.translate_import'],
['locale.translate_export'],
['locale.settings'],
];
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\locale\Unit;
use Drupal\locale\SourceString;
use Drupal\locale\StringStorageException;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\locale\StringBase
* @group locale
*/
class StringBaseTest extends UnitTestCase {
/**
* @covers ::save
*/
public function testSaveWithoutStorage(): void {
$string = new SourceString(['source' => 'test']);
$this->expectException(StringStorageException::class);
$this->expectExceptionMessage('The string cannot be saved because its not bound to a storage: test');
$string->save();
}
/**
* @covers ::delete
*/
public function testDeleteWithoutStorage(): void {
$string = new SourceString(['lid' => 1, 'source' => 'test']);
$this->expectException(StringStorageException::class);
$this->expectExceptionMessage('The string cannot be deleted because its not bound to a storage: test');
$string->delete();
}
}

View File

@@ -0,0 +1,13 @@
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 "@site is currently under maintenance. We should be back shortly. Thank you for your patience."
msgstr "Ons is tans besig met onderhoud op @site. Wees asseblief geduldig, ons sal binnekort weer terug wees."
msgid "Locale can translate"
msgstr "Locale can translate Afrikaans"

View File

@@ -0,0 +1,28 @@
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 "Monday"
msgstr "Montag"
msgid "Tuesday"
msgstr "Dienstag"
msgid "Wednesday"
msgstr "Mittwoch"
msgid "Thursday"
msgstr "Donnerstag"
msgid "Friday"
msgstr "Freitag"
msgid "Saturday"
msgstr "Samstag"
msgid "Sunday"
msgstr "Sonntag"

View File

@@ -0,0 +1,31 @@
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 "Monday"
msgstr "maandag"
msgid "Tuesday"
msgstr "dinsdag"
msgid "Wednesday"
msgstr "woensdag"
msgid "Thursday"
msgstr "donderdag"
msgid "Extra day"
msgstr "extra dag"
msgid "Friday"
msgstr "vrijdag"
msgid "Saturday"
msgstr "zaterdag"
msgid "Sunday"
msgstr "zondag"

View File

@@ -0,0 +1,40 @@
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 "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"
msgid "Allowed HTML source string"
msgstr "<strong>Allowed HTML translation string</strong>"
msgid "Another allowed HTML source string"
msgstr "<script>Disallowed HTML translation string</script>"
msgid "Source string for multiline translation"
msgstr ""
"Multiline translation string "
"to make sure that "
"import works with it."