Loading core/includes/install.core.inc +20 −7 Original line number Diff line number Diff line Loading @@ -450,7 +450,7 @@ function install_begin_request($class_loader, &$install_state): void { } /** @var \Drupal\Core\File\FileSystemInterface $file_system */ $file_system = $container->get('file_system'); $container->set('string_translator.file_translation', new FileTranslation($directory, $file_system)); $container->set('string_translator.file_translation', new FileTranslation($directory, $file_system, $install_state['parameters']['profile'] ?? NULL)); $container->get('string_translation') ->addTranslator($container->get('string_translator.file_translation')); Loading Loading @@ -1404,14 +1404,23 @@ function install_select_language(&$install_state) { */ function install_download_translation(&$install_state) { // Check whether all conditions are met to download. Download the translation // if possible. // for core if possible. $requirements = install_check_translations($install_state['parameters']['langcode'], $install_state['server_pattern']); // Render requirements if any warnings or errors returned. if ($output = install_display_requirements($install_state, $requirements)) { return $output; } // The download was successful, reload the page in the new language. $profile = $install_state['parameters']['profile'] ?? NULL; $profile_path = $install_state['profiles'][$profile]?->getPath(); $profile_version = $install_state['profiles'][$profile]?->info['version'] ?? NULL; // If we're using a custom profile and we know its version, try to download // that translation too. If we can't, proceed without it. if (isset($profile, $profile_path, $profile_version) && !str_starts_with($profile_path, 'core/')) { install_check_translations($install_state['parameters']['langcode'], $install_state['server_pattern'], $profile, $profile_version); } // At least one download was successful, reload the page in the new language. $install_state['translations'][$install_state['parameters']['langcode']] = TRUE; if ($install_state['interactive']) { install_goto(install_redirect_url($install_state)); Loading Loading @@ -1846,12 +1855,18 @@ function _install_module_batch(array $modules, array $module_names, array &$cont * Language code to check for download. * @param string $server_pattern * Server access pattern (to replace language code, version number, etc. in). * @param string $project * (optional) The name of the project for which to download translations. * Defaults to `drupal` (i.e., core). * @param string $version * (optional) The version of the project for which to download translations. * Defaults to \Drupal::VERSION. * * @return array * Requirements compliance array. If the translation cannot be downloaded this * will contain a requirements error with detailed information. */ function install_check_translations($langcode, $server_pattern): array { function install_check_translations($langcode, $server_pattern, string $project = 'drupal', string $version = \Drupal::VERSION): array { $requirements = []; $readable = FALSE; Loading @@ -1878,11 +1893,9 @@ function install_check_translations($langcode, $server_pattern): array { $translations_directory_exists = TRUE; } $version = \Drupal::VERSION; // Build URL for the translation file and the translation server. $variables = [ '%project' => 'drupal', '%project' => $project, '%version' => $version, '%core' => 'all', '%language' => $langcode, Loading core/lib/Drupal/Core/StringTranslation/Translator/FileTranslation.php +11 −20 Original line number Diff line number Diff line Loading @@ -17,32 +17,22 @@ */ class FileTranslation extends StaticTranslation { /** * Directory to find translation files in the file system. * * @var string */ protected $directory; /** * The file system. * * @var \Drupal\Core\File\FileSystemInterface */ protected $fileSystem; /** * Constructs a StaticTranslation object. * * @param string $directory * The directory to retrieve file translations from. * @param \Drupal\Core\File\FileSystemInterface $file_system * @param \Drupal\Core\File\FileSystemInterface $fileSystem * The file system service. * @param string|null $profile * The machine name of the install profile, or NULL if not known. */ public function __construct($directory, FileSystemInterface $file_system) { public function __construct( protected string $directory, protected FileSystemInterface $fileSystem, protected ?string $profile = NULL, ) { parent::__construct(); $this->directory = $directory; $this->fileSystem = $file_system; } /** Loading Loading @@ -100,10 +90,11 @@ public function findTranslationFiles($langcode = NULL) { * String file pattern. */ protected function getTranslationFilesPattern($langcode = NULL) { // The file name matches: drupal-[release version].[language code].po $project = $this->profile ? "(drupal|$this->profile)" : 'drupal'; // Matches (drupal|[profile])-[any-version].[language code].po // When provided the $langcode is use as language code. If not provided all // language codes will match. return '!drupal-[0-9]+\.[0-9]+(\.([0-9]+|x))?(-[a-z]+[0-9]*)?\.' . (!empty($langcode) ? preg_quote($langcode, '!') : LanguageInterface::VALID_LANGCODE_REGEX) . '\.po$!'; return '!' . $project . '-.+\.' . (!empty($langcode) ? preg_quote($langcode, '!') : LanguageInterface::VALID_LANGCODE_REGEX) . '\.po$!'; } /** Loading core/tests/Drupal/KernelTests/Core/Installer/InstallerTranslationDownloadTest.php 0 → 100644 +191 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\KernelTests\Core\Installer; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\StringTranslation\Translator\FileTranslation; use Drupal\KernelTests\KernelTestBase; use GuzzleHttp\Client; use GuzzleHttp\Psr7\Response; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; use PHPUnit\Framework\Attributes\TestWith; use Psr\Http\Message\RequestInterface; /** * Tests that profile translations are downloaded by the installer. */ #[Group('Installer')] #[RunTestsInSeparateProcesses] class InstallerTranslationDownloadTest extends KernelTestBase { /** * The original error handler, which will be overwritten by the installer. * * @var callable|null */ private $errorHandler; /** * An array of files that were requested, and by which methods. */ private array $requests = []; /** * The parameters to pass to install_drupal(). */ private array $installParameters = [ 'parameters' => [ 'langcode' => 'de', ], 'forms' => [ 'install_configure_form' => [ 'site_name' => 'Test', 'site_mail' => 'admin@example.com', 'account' => [ 'name' => 'admin', 'mail' => 'admin@example.com', 'pass' => [ 'pass1' => 'admin', 'pass2' => 'admin', ], ], ], ], 'interactive' => FALSE, ]; /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); // We will be calling installer functions directly. require_once 'core/includes/install.core.inc'; // We need to restore the error handler before tearDown, or the testing // system will yell at us. $this->errorHandler = get_error_handler(); } /** * {@inheritdoc} */ protected function tearDown(): void { set_error_handler($this->errorHandler); parent::tearDown(); } /** * {@inheritdoc} */ public function register(ContainerBuilder $container): void { $handler = function (RequestInterface $request): Response { $this->requests[] = $request->getMethod() . ' ' . basename($request->getUri()->getPath()); // We don't care about the response; we're just ensuring that certain // requests are made. return new Response(body: "msgid: \"\"\nmsgstr \"\""); }; $client = new Client(['handler' => $handler]); $container->set('http_client', $client); parent::register($container); } /** * Tests that the translations are downloaded for a custom profile. * * @param string $version * The version of the custom profile. */ #[TestWith(['1.2.3'], 'Semantic version')] #[TestWith(['2.x-dev'], 'Dev branch with -dev suffix')] #[TestWith(['2.x'], 'Dev branch without -dev suffix')] #[TestWith(['8.x-1.3'], 'Legacy tagged version')] #[TestWith(['2.1.0-alpha2'], 'Pre-release semantic version')] #[TestWith(['8.x-1.x-dev'], 'Legacy dev branch with -dev suffix')] #[TestWith(['8.x-1.x'], 'Legacy dev branch without -dev suffix')] public function testCustomProfileTranslationsAreDownloaded(string $version): void { // In kernel tests, the site directory is a VFS URI. We need to make it real // so the Extension class can access it. $pathname = parse_url($this->siteDirectory, PHP_URL_PATH); $pathname = trim($pathname, '/'); $pathname .= '/profiles/foo'; mkdir($pathname, recursive: TRUE); // Make a fake, non-core profile and explicitly expose it to the profile // discovery. $pathname .= '/foo.info.yml'; $info = <<<EOF name: Test type: profile version: $version core_version_requirement: '*' EOF; file_put_contents($pathname, $info); // We just created a new profile, but extensions have already been scanned. // Therefore, clear the extension discovery cache so the installer can // find the new profile. (new \ReflectionProperty(ExtensionDiscovery::class, 'files')) ->setValue(NULL, []); // Expose our fake profile directly to the installer. $this->installParameters['profiles']['foo'] = new Extension($this->root, 'profile', $pathname); $this->installParameters['parameters']['profile'] = 'foo'; install_drupal($this->classLoader, $this->installParameters); // Four requests should have been made: a HEAD and a GET request for each // of two files. $core_version = \Drupal::VERSION; $expected_requests = [ "HEAD drupal-$core_version.de.po", "GET drupal-$core_version.de.po", "HEAD foo-$version.de.po", "GET foo-$version.de.po", ]; $this->assertSame($expected_requests, $this->requests); // Confirm that the files were actually downloaded. $destination = dirname($pathname, 3) . '/files/translations'; $this->assertFileExists($destination . "/drupal-$core_version.de.po"); $this->assertFileExists($destination . "/foo-$version.de.po"); // Put another unrelated translation file into the directory to ensure that // FileTranslation doesn't see it. touch($destination . "/unrelated-$version.de.po"); // Confirm that the FileTranslation service used during the early installer // finds the downloaded files. $translation = new FileTranslation( $destination, \Drupal::service(FileSystemInterface::class), 'foo', ); $found_translations = array_column($translation->findTranslationFiles(), 'filename'); sort($found_translations); $expected_translations = [ "drupal-$core_version.de.po", "foo-$version.de.po", ]; $this->assertSame($expected_translations, $found_translations); } /** * Tests that translations are not downloaded for core profiles. */ public function testCoreProfileTranslationsAreNotDownloaded(): void { $this->installParameters['parameters']['profile'] = 'testing'; install_drupal($this->classLoader, $this->installParameters); $this->assertCount(2, $this->requests); $this->assertStringStartsWith("HEAD drupal-", $this->requests[0]); $this->assertStringStartsWith("GET drupal-", $this->requests[1]); } } Loading
core/includes/install.core.inc +20 −7 Original line number Diff line number Diff line Loading @@ -450,7 +450,7 @@ function install_begin_request($class_loader, &$install_state): void { } /** @var \Drupal\Core\File\FileSystemInterface $file_system */ $file_system = $container->get('file_system'); $container->set('string_translator.file_translation', new FileTranslation($directory, $file_system)); $container->set('string_translator.file_translation', new FileTranslation($directory, $file_system, $install_state['parameters']['profile'] ?? NULL)); $container->get('string_translation') ->addTranslator($container->get('string_translator.file_translation')); Loading Loading @@ -1404,14 +1404,23 @@ function install_select_language(&$install_state) { */ function install_download_translation(&$install_state) { // Check whether all conditions are met to download. Download the translation // if possible. // for core if possible. $requirements = install_check_translations($install_state['parameters']['langcode'], $install_state['server_pattern']); // Render requirements if any warnings or errors returned. if ($output = install_display_requirements($install_state, $requirements)) { return $output; } // The download was successful, reload the page in the new language. $profile = $install_state['parameters']['profile'] ?? NULL; $profile_path = $install_state['profiles'][$profile]?->getPath(); $profile_version = $install_state['profiles'][$profile]?->info['version'] ?? NULL; // If we're using a custom profile and we know its version, try to download // that translation too. If we can't, proceed without it. if (isset($profile, $profile_path, $profile_version) && !str_starts_with($profile_path, 'core/')) { install_check_translations($install_state['parameters']['langcode'], $install_state['server_pattern'], $profile, $profile_version); } // At least one download was successful, reload the page in the new language. $install_state['translations'][$install_state['parameters']['langcode']] = TRUE; if ($install_state['interactive']) { install_goto(install_redirect_url($install_state)); Loading Loading @@ -1846,12 +1855,18 @@ function _install_module_batch(array $modules, array $module_names, array &$cont * Language code to check for download. * @param string $server_pattern * Server access pattern (to replace language code, version number, etc. in). * @param string $project * (optional) The name of the project for which to download translations. * Defaults to `drupal` (i.e., core). * @param string $version * (optional) The version of the project for which to download translations. * Defaults to \Drupal::VERSION. * * @return array * Requirements compliance array. If the translation cannot be downloaded this * will contain a requirements error with detailed information. */ function install_check_translations($langcode, $server_pattern): array { function install_check_translations($langcode, $server_pattern, string $project = 'drupal', string $version = \Drupal::VERSION): array { $requirements = []; $readable = FALSE; Loading @@ -1878,11 +1893,9 @@ function install_check_translations($langcode, $server_pattern): array { $translations_directory_exists = TRUE; } $version = \Drupal::VERSION; // Build URL for the translation file and the translation server. $variables = [ '%project' => 'drupal', '%project' => $project, '%version' => $version, '%core' => 'all', '%language' => $langcode, Loading
core/lib/Drupal/Core/StringTranslation/Translator/FileTranslation.php +11 −20 Original line number Diff line number Diff line Loading @@ -17,32 +17,22 @@ */ class FileTranslation extends StaticTranslation { /** * Directory to find translation files in the file system. * * @var string */ protected $directory; /** * The file system. * * @var \Drupal\Core\File\FileSystemInterface */ protected $fileSystem; /** * Constructs a StaticTranslation object. * * @param string $directory * The directory to retrieve file translations from. * @param \Drupal\Core\File\FileSystemInterface $file_system * @param \Drupal\Core\File\FileSystemInterface $fileSystem * The file system service. * @param string|null $profile * The machine name of the install profile, or NULL if not known. */ public function __construct($directory, FileSystemInterface $file_system) { public function __construct( protected string $directory, protected FileSystemInterface $fileSystem, protected ?string $profile = NULL, ) { parent::__construct(); $this->directory = $directory; $this->fileSystem = $file_system; } /** Loading Loading @@ -100,10 +90,11 @@ public function findTranslationFiles($langcode = NULL) { * String file pattern. */ protected function getTranslationFilesPattern($langcode = NULL) { // The file name matches: drupal-[release version].[language code].po $project = $this->profile ? "(drupal|$this->profile)" : 'drupal'; // Matches (drupal|[profile])-[any-version].[language code].po // When provided the $langcode is use as language code. If not provided all // language codes will match. return '!drupal-[0-9]+\.[0-9]+(\.([0-9]+|x))?(-[a-z]+[0-9]*)?\.' . (!empty($langcode) ? preg_quote($langcode, '!') : LanguageInterface::VALID_LANGCODE_REGEX) . '\.po$!'; return '!' . $project . '-.+\.' . (!empty($langcode) ? preg_quote($langcode, '!') : LanguageInterface::VALID_LANGCODE_REGEX) . '\.po$!'; } /** Loading
core/tests/Drupal/KernelTests/Core/Installer/InstallerTranslationDownloadTest.php 0 → 100644 +191 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\KernelTests\Core\Installer; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\StringTranslation\Translator\FileTranslation; use Drupal\KernelTests\KernelTestBase; use GuzzleHttp\Client; use GuzzleHttp\Psr7\Response; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; use PHPUnit\Framework\Attributes\TestWith; use Psr\Http\Message\RequestInterface; /** * Tests that profile translations are downloaded by the installer. */ #[Group('Installer')] #[RunTestsInSeparateProcesses] class InstallerTranslationDownloadTest extends KernelTestBase { /** * The original error handler, which will be overwritten by the installer. * * @var callable|null */ private $errorHandler; /** * An array of files that were requested, and by which methods. */ private array $requests = []; /** * The parameters to pass to install_drupal(). */ private array $installParameters = [ 'parameters' => [ 'langcode' => 'de', ], 'forms' => [ 'install_configure_form' => [ 'site_name' => 'Test', 'site_mail' => 'admin@example.com', 'account' => [ 'name' => 'admin', 'mail' => 'admin@example.com', 'pass' => [ 'pass1' => 'admin', 'pass2' => 'admin', ], ], ], ], 'interactive' => FALSE, ]; /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); // We will be calling installer functions directly. require_once 'core/includes/install.core.inc'; // We need to restore the error handler before tearDown, or the testing // system will yell at us. $this->errorHandler = get_error_handler(); } /** * {@inheritdoc} */ protected function tearDown(): void { set_error_handler($this->errorHandler); parent::tearDown(); } /** * {@inheritdoc} */ public function register(ContainerBuilder $container): void { $handler = function (RequestInterface $request): Response { $this->requests[] = $request->getMethod() . ' ' . basename($request->getUri()->getPath()); // We don't care about the response; we're just ensuring that certain // requests are made. return new Response(body: "msgid: \"\"\nmsgstr \"\""); }; $client = new Client(['handler' => $handler]); $container->set('http_client', $client); parent::register($container); } /** * Tests that the translations are downloaded for a custom profile. * * @param string $version * The version of the custom profile. */ #[TestWith(['1.2.3'], 'Semantic version')] #[TestWith(['2.x-dev'], 'Dev branch with -dev suffix')] #[TestWith(['2.x'], 'Dev branch without -dev suffix')] #[TestWith(['8.x-1.3'], 'Legacy tagged version')] #[TestWith(['2.1.0-alpha2'], 'Pre-release semantic version')] #[TestWith(['8.x-1.x-dev'], 'Legacy dev branch with -dev suffix')] #[TestWith(['8.x-1.x'], 'Legacy dev branch without -dev suffix')] public function testCustomProfileTranslationsAreDownloaded(string $version): void { // In kernel tests, the site directory is a VFS URI. We need to make it real // so the Extension class can access it. $pathname = parse_url($this->siteDirectory, PHP_URL_PATH); $pathname = trim($pathname, '/'); $pathname .= '/profiles/foo'; mkdir($pathname, recursive: TRUE); // Make a fake, non-core profile and explicitly expose it to the profile // discovery. $pathname .= '/foo.info.yml'; $info = <<<EOF name: Test type: profile version: $version core_version_requirement: '*' EOF; file_put_contents($pathname, $info); // We just created a new profile, but extensions have already been scanned. // Therefore, clear the extension discovery cache so the installer can // find the new profile. (new \ReflectionProperty(ExtensionDiscovery::class, 'files')) ->setValue(NULL, []); // Expose our fake profile directly to the installer. $this->installParameters['profiles']['foo'] = new Extension($this->root, 'profile', $pathname); $this->installParameters['parameters']['profile'] = 'foo'; install_drupal($this->classLoader, $this->installParameters); // Four requests should have been made: a HEAD and a GET request for each // of two files. $core_version = \Drupal::VERSION; $expected_requests = [ "HEAD drupal-$core_version.de.po", "GET drupal-$core_version.de.po", "HEAD foo-$version.de.po", "GET foo-$version.de.po", ]; $this->assertSame($expected_requests, $this->requests); // Confirm that the files were actually downloaded. $destination = dirname($pathname, 3) . '/files/translations'; $this->assertFileExists($destination . "/drupal-$core_version.de.po"); $this->assertFileExists($destination . "/foo-$version.de.po"); // Put another unrelated translation file into the directory to ensure that // FileTranslation doesn't see it. touch($destination . "/unrelated-$version.de.po"); // Confirm that the FileTranslation service used during the early installer // finds the downloaded files. $translation = new FileTranslation( $destination, \Drupal::service(FileSystemInterface::class), 'foo', ); $found_translations = array_column($translation->findTranslationFiles(), 'filename'); sort($found_translations); $expected_translations = [ "drupal-$core_version.de.po", "foo-$version.de.po", ]; $this->assertSame($expected_translations, $found_translations); } /** * Tests that translations are not downloaded for core profiles. */ public function testCoreProfileTranslationsAreNotDownloaded(): void { $this->installParameters['parameters']['profile'] = 'testing'; install_drupal($this->classLoader, $this->installParameters); $this->assertCount(2, $this->requests); $this->assertStringStartsWith("HEAD drupal-", $this->requests[0]); $this->assertStringStartsWith("GET drupal-", $this->requests[1]); } }