From fa85870db50cb7c44c9a36d6f1caed005a206d41 Mon Sep 17 00:00:00 2001 From: Adam G-H <32250-phenaproxima@users.noreply.drupalcode.org> Date: Wed, 19 Jul 2023 17:00:01 +0000 Subject: [PATCH] Issue #3368808 by phenaproxima: Override Composer Stager's TranslatableFactory to return Drupal's TranslatableMarkup --- package_manager/package_manager.install | 8 ++- package_manager/package_manager.services.yml | 4 ++ .../src/PackageManagerServiceProvider.php | 1 + .../src/TranslatableStringAdapter.php | 53 +++++++++++++++ .../src/TranslatableStringFactory.php | 65 +++++++++++++++++++ package_manager/src/ValidationResult.php | 6 +- .../src/Kernel/ComposerInspectorTest.php | 5 +- .../tests/src/Kernel/ServicesTest.php | 3 + .../tests/src/Kernel/SymlinkValidatorTest.php | 34 ++++++---- .../src/Kernel/TranslatableStringTest.php | 41 ++++++++++++ 10 files changed, 206 insertions(+), 14 deletions(-) create mode 100644 package_manager/src/TranslatableStringAdapter.php create mode 100644 package_manager/src/TranslatableStringFactory.php create mode 100644 package_manager/tests/src/Kernel/TranslatableStringTest.php diff --git a/package_manager/package_manager.install b/package_manager/package_manager.install index 303e3de373..d022e7f019 100644 --- a/package_manager/package_manager.install +++ b/package_manager/package_manager.install @@ -10,6 +10,7 @@ declare(strict_types = 1); use Drupal\package_manager\ComposerInspector; use Drupal\package_manager\Exception\StageFailureMarkerException; use PhpTuf\ComposerStager\API\Core\BeginnerInterface; +use PhpTuf\ComposerStager\API\Exception\ExceptionInterface; use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; /** @@ -62,10 +63,15 @@ function package_manager_requirements(string $phase) { ]; } catch (\Throwable $e) { + // All Composer Stager exceptions are translatable. + $message = $e instanceof ExceptionInterface + ? $e->getTranslatableMessage() + : $e->getMessage(); + $requirements['package_manager_composer'] = [ 'title' => $title, 'description' => t('Composer was not found. The error message was: @message', [ - '@message' => $e->getMessage(), + '@message' => $message, ]), 'severity' => REQUIREMENT_ERROR, ]; diff --git a/package_manager/package_manager.services.yml b/package_manager/package_manager.services.yml index 43e0aa906b..23d28fe557 100644 --- a/package_manager/package_manager.services.yml +++ b/package_manager/package_manager.services.yml @@ -16,12 +16,16 @@ services: public: false Drupal\package_manager\FileSyncerFactory: public: false + Drupal\package_manager\TranslatableStringFactory: + public: false PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface: alias: 'Drupal\package_manager\ExecutableFinder' PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface: alias: 'Drupal\package_manager\ProcessFactory' PhpTuf\ComposerStager\API\FileSyncer\Service\FileSyncerInterface: factory: ['@Drupal\package_manager\FileSyncerFactory', 'create'] + PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface: + alias: 'Drupal\package_manager\TranslatableStringFactory' logger.channel.package_manager: parent: logger.channel_base arguments: diff --git a/package_manager/src/PackageManagerServiceProvider.php b/package_manager/src/PackageManagerServiceProvider.php index ba31f9985b..3caf33e14a 100644 --- a/package_manager/src/PackageManagerServiceProvider.php +++ b/package_manager/src/PackageManagerServiceProvider.php @@ -111,6 +111,7 @@ final class PackageManagerServiceProvider extends ServiceProviderBase { 'theme_handler' => 'Drupal\Core\Extension\ThemeHandlerInterface', 'cron' => 'Drupal\Core\CronInterface', 'logger.factory' => 'Drupal\Core\Logger\LoggerChannelFactoryInterface', + 'string_translation' => 'Drupal\Core\StringTranslation\TranslationInterface', ]; foreach ($aliases as $service_id => $alias) { if (!$container->hasAlias($alias)) { diff --git a/package_manager/src/TranslatableStringAdapter.php b/package_manager/src/TranslatableStringAdapter.php new file mode 100644 index 0000000000..9db2f5ac95 --- /dev/null +++ b/package_manager/src/TranslatableStringAdapter.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\package_manager; + +use Drupal\Core\StringTranslation\TranslatableMarkup; +use PhpTuf\ComposerStager\API\Translation\Service\TranslatorInterface; +use PhpTuf\ComposerStager\API\Translation\Value\TranslatableInterface; +use PhpTuf\ComposerStager\API\Translation\Value\TranslationParametersInterface; + +/** + * An adapter for interoperable string translation. + * + * This class is designed to adapt Drupal's style of string translation so it + * can be used with the Symfony-inspired architecture used by Composer Stager. + * + * If this object is cast to a string, it will be translated by Drupal's + * translation system. It will ONLY be translated by Composer Stager if the + * trans() method is explicitly called. + * + * @internal + * This is an internal part of Package Manager and may be changed or removed + * at any time without warning. External code should not interact with this + * class. + */ +final class TranslatableStringAdapter extends TranslatableMarkup implements TranslatableInterface, TranslationParametersInterface { + + /** + * {@inheritdoc} + */ + public function getAll(): array { + return $this->getArguments(); + } + + /** + * {@inheritdoc} + */ + public function trans(TranslatorInterface $translator, ?string $locale = NULL): string { + // This method is NEVER used by Drupal to translate the underlying string; + // it exists solely for Composer Stager's translation system to + // transparently translate Drupal strings using its own architecture. + return $translator->trans( + $this->getUntranslatedString(), + $this, + // The 'context' option is the closest analogue to the Symfony-inspired + // concept of translation domains. + $this->getOption('context'), + $locale ?? $this->getOption('langcode'), + ); + } + +} diff --git a/package_manager/src/TranslatableStringFactory.php b/package_manager/src/TranslatableStringFactory.php new file mode 100644 index 0000000000..8b2f6a2282 --- /dev/null +++ b/package_manager/src/TranslatableStringFactory.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\package_manager; + +use Drupal\Core\StringTranslation\TranslationInterface; +use PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface; +use PhpTuf\ComposerStager\API\Translation\Service\DomainOptionsInterface; +use PhpTuf\ComposerStager\API\Translation\Value\TranslatableInterface; +use PhpTuf\ComposerStager\API\Translation\Value\TranslationParametersInterface; +use PhpTuf\ComposerStager\Internal\Translation\Factory\TranslatableFactory as StagerTranslatableFactory; + +/** + * Creates translatable strings that can interoperate with Composer Stager. + * + * @internal + * This is an internal part of Package Manager and may be changed or removed + * at any time without warning. External code should not interact with this + * class. + */ +final class TranslatableStringFactory implements TranslatableFactoryInterface { + + /** + * Constructs a TranslatableStringFactory object. + * + * @param \PhpTuf\ComposerStager\Internal\Translation\Factory\TranslatableFactory $decorated + * The decorated translatable factory service. + * @param \Drupal\Core\StringTranslation\TranslationInterface $translation + * The string translation service. + */ + public function __construct( + private readonly StagerTranslatableFactory $decorated, + private readonly TranslationInterface $translation, + ) {} + + /** + * {@inheritdoc} + */ + public function createDomainOptions(): DomainOptionsInterface { + return $this->decorated->createDomainOptions(); + } + + /** + * {@inheritdoc} + */ + public function createTranslatableMessage(string $message, ?TranslationParametersInterface $parameters = NULL, ?string $domain = NULL,): TranslatableInterface { + return new TranslatableStringAdapter( + $message, + $parameters?->getAll() ?? [], + // TranslatableMarkup's 'context' option is the closest analogue to the + // $domain parameter. + ['context' => $domain ?? ''], + $this->translation, + ); + } + + /** + * {@inheritdoc} + */ + public function createTranslationParameters(array $parameters = []): TranslationParametersInterface { + return $this->decorated->createTranslationParameters($parameters); + } + +} diff --git a/package_manager/src/ValidationResult.php b/package_manager/src/ValidationResult.php index a9854a075a..3292a4a9ec 100644 --- a/package_manager/src/ValidationResult.php +++ b/package_manager/src/ValidationResult.php @@ -7,6 +7,7 @@ namespace Drupal\package_manager; use Drupal\Component\Assertion\Inspector; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\system\SystemManager; +use PhpTuf\ComposerStager\API\Exception\ExceptionInterface; /** * A value object to contain the results of a validation. @@ -72,7 +73,10 @@ final class ValidationResult { * @return static */ public static function createErrorFromThrowable(\Throwable $throwable, ?TranslatableMarkup $summary = NULL): static { - return new static(SystemManager::REQUIREMENT_ERROR, [$throwable->getMessage()], $summary, FALSE); + // All Composer Stager exceptions are translatable. + $is_translatable = $throwable instanceof ExceptionInterface; + $message = $is_translatable ? $throwable->getTranslatableMessage() : $throwable->getMessage(); + return new static(SystemManager::REQUIREMENT_ERROR, [$message], $summary, $is_translatable); } /** diff --git a/package_manager/tests/src/Kernel/ComposerInspectorTest.php b/package_manager/tests/src/Kernel/ComposerInspectorTest.php index 5d4c0de288..ee3106a38e 100644 --- a/package_manager/tests/src/Kernel/ComposerInspectorTest.php +++ b/package_manager/tests/src/Kernel/ComposerInspectorTest.php @@ -318,7 +318,10 @@ class ComposerInspectorTest extends PackageManagerKernelTestBase { } catch (ComposerNotReadyException $e) { $this->assertSame($project_root, $e->workingDir); - $this->assertStringContainsString('composer.json" does not match the expected JSON schema', $e->getMessage()); + // The exception message is translated by Composer Stager and HTML-escaped + // by Drupal's markup system, which is why there's a " in the + // final exception message. + $this->assertStringContainsString('composer.json" does not match the expected JSON schema', $e->getMessage()); $this->assertStringContainsString('prefer-stable : String value found, but a boolean is required', $e->getPrevious()?->getMessage()); } } diff --git a/package_manager/tests/src/Kernel/ServicesTest.php b/package_manager/tests/src/Kernel/ServicesTest.php index 26862671b2..862af87396 100644 --- a/package_manager/tests/src/Kernel/ServicesTest.php +++ b/package_manager/tests/src/Kernel/ServicesTest.php @@ -7,9 +7,11 @@ namespace Drupal\Tests\package_manager\Kernel; use Drupal\KernelTests\KernelTestBase; use Drupal\package_manager\ExecutableFinder; use Drupal\package_manager\ProcessFactory; +use Drupal\package_manager\TranslatableStringFactory; use Drupal\Tests\package_manager\Traits\AssertPreconditionsTrait; use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; +use PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface; /** * Tests that Package Manager services are wired correctly. @@ -44,6 +46,7 @@ class ServicesTest extends KernelTestBase { $overrides = [ ExecutableFinderInterface::class => ExecutableFinder::class, ProcessFactoryInterface::class => ProcessFactory::class, + TranslatableFactoryInterface::class => TranslatableStringFactory::class, ]; foreach ($overrides as $interface => $expected_class) { $this->assertInstanceOf($expected_class, $this->container->get($interface)); diff --git a/package_manager/tests/src/Kernel/SymlinkValidatorTest.php b/package_manager/tests/src/Kernel/SymlinkValidatorTest.php index 1f59dc4c8a..bd7de89489 100644 --- a/package_manager/tests/src/Kernel/SymlinkValidatorTest.php +++ b/package_manager/tests/src/Kernel/SymlinkValidatorTest.php @@ -47,8 +47,10 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase { link($project_root . '/composer.json', $project_root . '/composer.link'); $result = ValidationResult::createError([ - t('The active directory at @dir contains hard links, which is not supported. The first one is @dir/composer.json.', [ - '@dir' => $project_root, + t('The %which directory at %dir contains hard links, which is not supported. The first one is %file.', [ + '%which' => 'active', + '%dir' => $project_root, + '%file' => $project_root . '/composer.json', ]), ]); $this->assertStatusCheckResults([$result]); @@ -63,8 +65,10 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase { symlink($project_root . '/composer.json', $project_root . '/composer.link'); $result = ValidationResult::createError([ - t('The active directory at @dir contains absolute links, which is not supported. The first one is @dir/composer.link.', [ - '@dir' => $project_root, + t('The %which directory at %dir contains absolute links, which is not supported. The first one is %file.', [ + '%which' => 'active', + '%dir' => $project_root, + '%file' => $project_root . '/composer.link', ]), ]); $this->assertStatusCheckResults([$result]); @@ -84,8 +88,10 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase { chdir($project_root); symlink('../hello.txt', 'fail.txt'); $result = ValidationResult::createError([ - t('The active directory at @dir contains links that point outside the codebase, which is not supported. The first one is @dir/fail.txt.', [ - '@dir' => $project_root, + t('The %which directory at %dir contains links that point outside the codebase, which is not supported. The first one is %file.', [ + '%which' => 'active', + '%dir' => $project_root, + '%file' => $project_root . '/fail.txt', ]), ]); $this->assertStatusCheckResults([$result]); @@ -111,8 +117,10 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase { symlink('../hello.txt', 'fail.txt'); $result = ValidationResult::createError([ - t('The staging directory at @dir contains links that point outside the codebase, which is not supported. The first one is @dir/fail.txt.', [ - '@dir' => $stage_dir, + t('The %which directory at %dir contains links that point outside the codebase, which is not supported. The first one is %file.', [ + '%which' => 'staging', + '%dir' => $stage_dir, + '%file' => $stage_dir . '/fail.txt', ]), ]); try { @@ -136,7 +144,9 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase { 'php', [ ValidationResult::createError([ - t('The active directory at <PROJECT_ROOT> contains symlinks that point to a directory, which is not supported. The first one is <PROJECT_ROOT>/modules/custom/example_module.'), + t('The %which directory at <em class="placeholder"><PROJECT_ROOT></em> contains symlinks that point to a directory, which is not supported. The first one is <em class="placeholder"><PROJECT_ROOT>/modules/custom/example_module</em>.', [ + '%which' => 'active', + ]), ]), ], ], @@ -197,8 +207,10 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase { symlink('composer.json', 'composer.link'); $result = ValidationResult::createError([ - t('The active directory at @dir contains links, which is not supported on Windows. The first one is @dir/composer.link.', [ - '@dir' => $project_root, + t('The %which directory at %dir contains links, which is not supported on Windows. The first one is %file.', [ + '%which' => 'active', + '%dir' => $project_root, + '%file' => $project_root . '/composer.link', ]), ]); $this->assertStatusCheckResults([$result]); diff --git a/package_manager/tests/src/Kernel/TranslatableStringTest.php b/package_manager/tests/src/Kernel/TranslatableStringTest.php new file mode 100644 index 0000000000..d05e3043fe --- /dev/null +++ b/package_manager/tests/src/Kernel/TranslatableStringTest.php @@ -0,0 +1,41 @@ +<?php + +namespace Drupal\Tests\package_manager\Kernel; + +use Drupal\package_manager\TranslatableStringAdapter; +use Drupal\package_manager\TranslatableStringFactory; +use PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface; + +/** + * @covers \Drupal\package_manager\TranslatableStringFactory + * @covers \Drupal\package_manager\TranslatableStringAdapter + * + * @group package_manager + */ +class TranslatableStringTest extends PackageManagerKernelTestBase { + + /** + * Tests various ways of creating a translatable string. + */ + public function testCreateTranslatableString(): void { + // Ensure that we have properly overridden Composer Stager's factory. + $factory = $this->container->get(TranslatableFactoryInterface::class); + $this->assertInstanceOf(TranslatableStringFactory::class, $factory); + + /** @var \Drupal\package_manager\TranslatableStringAdapter $string */ + $string = $factory->createTranslatableMessage('This string has no parameters.'); + $this->assertInstanceOf(TranslatableStringAdapter::class, $string); + $this->assertEmpty($string->getArguments()); + $this->assertEmpty($string->getOption('context')); + $this->assertSame('This string has no parameters.', (string) $string); + + $parameters = $factory->createTranslationParameters([ + '%name' => 'Slim Shady', + ]); + $string = $factory->createTranslatableMessage('My name is %name.', $parameters, 'outer space'); + $this->assertSame($parameters->getAll(), $string->getArguments()); + $this->assertSame('outer space', $string->getOption('context')); + $this->assertSame('My name is <em class="placeholder">Slim Shady</em>.', (string) $string); + } + +} -- GitLab