Skip to content
Snippets Groups Projects
Commit fa85870d authored by Adam G-H's avatar Adam G-H
Browse files

Issue #3368808 by phenaproxima: Override Composer Stager's TranslatableFactory...

Issue #3368808 by phenaproxima: Override Composer Stager's TranslatableFactory to return Drupal's TranslatableMarkup
parent 4d2c1697
No related branches found
No related tags found
No related merge requests found
...@@ -10,6 +10,7 @@ declare(strict_types = 1); ...@@ -10,6 +10,7 @@ declare(strict_types = 1);
use Drupal\package_manager\ComposerInspector; use Drupal\package_manager\ComposerInspector;
use Drupal\package_manager\Exception\StageFailureMarkerException; use Drupal\package_manager\Exception\StageFailureMarkerException;
use PhpTuf\ComposerStager\API\Core\BeginnerInterface; use PhpTuf\ComposerStager\API\Core\BeginnerInterface;
use PhpTuf\ComposerStager\API\Exception\ExceptionInterface;
use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface;
/** /**
...@@ -62,10 +63,15 @@ function package_manager_requirements(string $phase) { ...@@ -62,10 +63,15 @@ function package_manager_requirements(string $phase) {
]; ];
} }
catch (\Throwable $e) { catch (\Throwable $e) {
// All Composer Stager exceptions are translatable.
$message = $e instanceof ExceptionInterface
? $e->getTranslatableMessage()
: $e->getMessage();
$requirements['package_manager_composer'] = [ $requirements['package_manager_composer'] = [
'title' => $title, 'title' => $title,
'description' => t('Composer was not found. The error message was: @message', [ 'description' => t('Composer was not found. The error message was: @message', [
'@message' => $e->getMessage(), '@message' => $message,
]), ]),
'severity' => REQUIREMENT_ERROR, 'severity' => REQUIREMENT_ERROR,
]; ];
......
...@@ -16,12 +16,16 @@ services: ...@@ -16,12 +16,16 @@ services:
public: false public: false
Drupal\package_manager\FileSyncerFactory: Drupal\package_manager\FileSyncerFactory:
public: false public: false
Drupal\package_manager\TranslatableStringFactory:
public: false
PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface: PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface:
alias: 'Drupal\package_manager\ExecutableFinder' alias: 'Drupal\package_manager\ExecutableFinder'
PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface: PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface:
alias: 'Drupal\package_manager\ProcessFactory' alias: 'Drupal\package_manager\ProcessFactory'
PhpTuf\ComposerStager\API\FileSyncer\Service\FileSyncerInterface: PhpTuf\ComposerStager\API\FileSyncer\Service\FileSyncerInterface:
factory: ['@Drupal\package_manager\FileSyncerFactory', 'create'] factory: ['@Drupal\package_manager\FileSyncerFactory', 'create']
PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface:
alias: 'Drupal\package_manager\TranslatableStringFactory'
logger.channel.package_manager: logger.channel.package_manager:
parent: logger.channel_base parent: logger.channel_base
arguments: arguments:
......
...@@ -111,6 +111,7 @@ final class PackageManagerServiceProvider extends ServiceProviderBase { ...@@ -111,6 +111,7 @@ final class PackageManagerServiceProvider extends ServiceProviderBase {
'theme_handler' => 'Drupal\Core\Extension\ThemeHandlerInterface', 'theme_handler' => 'Drupal\Core\Extension\ThemeHandlerInterface',
'cron' => 'Drupal\Core\CronInterface', 'cron' => 'Drupal\Core\CronInterface',
'logger.factory' => 'Drupal\Core\Logger\LoggerChannelFactoryInterface', 'logger.factory' => 'Drupal\Core\Logger\LoggerChannelFactoryInterface',
'string_translation' => 'Drupal\Core\StringTranslation\TranslationInterface',
]; ];
foreach ($aliases as $service_id => $alias) { foreach ($aliases as $service_id => $alias) {
if (!$container->hasAlias($alias)) { if (!$container->hasAlias($alias)) {
......
<?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'),
);
}
}
<?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);
}
}
...@@ -7,6 +7,7 @@ namespace Drupal\package_manager; ...@@ -7,6 +7,7 @@ namespace Drupal\package_manager;
use Drupal\Component\Assertion\Inspector; use Drupal\Component\Assertion\Inspector;
use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\system\SystemManager; use Drupal\system\SystemManager;
use PhpTuf\ComposerStager\API\Exception\ExceptionInterface;
/** /**
* A value object to contain the results of a validation. * A value object to contain the results of a validation.
...@@ -72,7 +73,10 @@ final class ValidationResult { ...@@ -72,7 +73,10 @@ final class ValidationResult {
* @return static * @return static
*/ */
public static function createErrorFromThrowable(\Throwable $throwable, ?TranslatableMarkup $summary = NULL): 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);
} }
/** /**
......
...@@ -318,7 +318,10 @@ class ComposerInspectorTest extends PackageManagerKernelTestBase { ...@@ -318,7 +318,10 @@ class ComposerInspectorTest extends PackageManagerKernelTestBase {
} }
catch (ComposerNotReadyException $e) { catch (ComposerNotReadyException $e) {
$this->assertSame($project_root, $e->workingDir); $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 &quot; in the
// final exception message.
$this->assertStringContainsString('composer.json&quot; does not match the expected JSON schema', $e->getMessage());
$this->assertStringContainsString('prefer-stable : String value found, but a boolean is required', $e->getPrevious()?->getMessage()); $this->assertStringContainsString('prefer-stable : String value found, but a boolean is required', $e->getPrevious()?->getMessage());
} }
} }
......
...@@ -7,9 +7,11 @@ namespace Drupal\Tests\package_manager\Kernel; ...@@ -7,9 +7,11 @@ namespace Drupal\Tests\package_manager\Kernel;
use Drupal\KernelTests\KernelTestBase; use Drupal\KernelTests\KernelTestBase;
use Drupal\package_manager\ExecutableFinder; use Drupal\package_manager\ExecutableFinder;
use Drupal\package_manager\ProcessFactory; use Drupal\package_manager\ProcessFactory;
use Drupal\package_manager\TranslatableStringFactory;
use Drupal\Tests\package_manager\Traits\AssertPreconditionsTrait; use Drupal\Tests\package_manager\Traits\AssertPreconditionsTrait;
use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface; use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface;
use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface; use PhpTuf\ComposerStager\API\Process\Factory\ProcessFactoryInterface;
use PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface;
/** /**
* Tests that Package Manager services are wired correctly. * Tests that Package Manager services are wired correctly.
...@@ -44,6 +46,7 @@ class ServicesTest extends KernelTestBase { ...@@ -44,6 +46,7 @@ class ServicesTest extends KernelTestBase {
$overrides = [ $overrides = [
ExecutableFinderInterface::class => ExecutableFinder::class, ExecutableFinderInterface::class => ExecutableFinder::class,
ProcessFactoryInterface::class => ProcessFactory::class, ProcessFactoryInterface::class => ProcessFactory::class,
TranslatableFactoryInterface::class => TranslatableStringFactory::class,
]; ];
foreach ($overrides as $interface => $expected_class) { foreach ($overrides as $interface => $expected_class) {
$this->assertInstanceOf($expected_class, $this->container->get($interface)); $this->assertInstanceOf($expected_class, $this->container->get($interface));
......
...@@ -47,8 +47,10 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase { ...@@ -47,8 +47,10 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase {
link($project_root . '/composer.json', $project_root . '/composer.link'); link($project_root . '/composer.json', $project_root . '/composer.link');
$result = ValidationResult::createError([ $result = ValidationResult::createError([
t('The active directory at @dir contains hard links, which is not supported. The first one is @dir/composer.json.', [ t('The %which directory at %dir contains hard links, which is not supported. The first one is %file.', [
'@dir' => $project_root, '%which' => 'active',
'%dir' => $project_root,
'%file' => $project_root . '/composer.json',
]), ]),
]); ]);
$this->assertStatusCheckResults([$result]); $this->assertStatusCheckResults([$result]);
...@@ -63,8 +65,10 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase { ...@@ -63,8 +65,10 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase {
symlink($project_root . '/composer.json', $project_root . '/composer.link'); symlink($project_root . '/composer.json', $project_root . '/composer.link');
$result = ValidationResult::createError([ $result = ValidationResult::createError([
t('The active directory at @dir contains absolute links, which is not supported. The first one is @dir/composer.link.', [ t('The %which directory at %dir contains absolute links, which is not supported. The first one is %file.', [
'@dir' => $project_root, '%which' => 'active',
'%dir' => $project_root,
'%file' => $project_root . '/composer.link',
]), ]),
]); ]);
$this->assertStatusCheckResults([$result]); $this->assertStatusCheckResults([$result]);
...@@ -84,8 +88,10 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase { ...@@ -84,8 +88,10 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase {
chdir($project_root); chdir($project_root);
symlink('../hello.txt', 'fail.txt'); symlink('../hello.txt', 'fail.txt');
$result = ValidationResult::createError([ $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.', [ t('The %which directory at %dir contains links that point outside the codebase, which is not supported. The first one is %file.', [
'@dir' => $project_root, '%which' => 'active',
'%dir' => $project_root,
'%file' => $project_root . '/fail.txt',
]), ]),
]); ]);
$this->assertStatusCheckResults([$result]); $this->assertStatusCheckResults([$result]);
...@@ -111,8 +117,10 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase { ...@@ -111,8 +117,10 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase {
symlink('../hello.txt', 'fail.txt'); symlink('../hello.txt', 'fail.txt');
$result = ValidationResult::createError([ $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.', [ t('The %which directory at %dir contains links that point outside the codebase, which is not supported. The first one is %file.', [
'@dir' => $stage_dir, '%which' => 'staging',
'%dir' => $stage_dir,
'%file' => $stage_dir . '/fail.txt',
]), ]),
]); ]);
try { try {
...@@ -136,7 +144,9 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase { ...@@ -136,7 +144,9 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase {
'php', 'php',
[ [
ValidationResult::createError([ 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 { ...@@ -197,8 +207,10 @@ class SymlinkValidatorTest extends PackageManagerKernelTestBase {
symlink('composer.json', 'composer.link'); symlink('composer.json', 'composer.link');
$result = ValidationResult::createError([ $result = ValidationResult::createError([
t('The active directory at @dir contains links, which is not supported on Windows. The first one is @dir/composer.link.', [ t('The %which directory at %dir contains links, which is not supported on Windows. The first one is %file.', [
'@dir' => $project_root, '%which' => 'active',
'%dir' => $project_root,
'%file' => $project_root . '/composer.link',
]), ]),
]); ]);
$this->assertStatusCheckResults([$result]); $this->assertStatusCheckResults([$result]);
......
<?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);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment