[Translator] Refactor TranslationsDumper options from __constructor and setters, to dump method

This commit is contained in:
Hugo Alliaume
2025-12-19 08:24:27 +01:00
parent 738bc91d5a
commit 9472902436
7 changed files with 114 additions and 68 deletions

View File

@@ -49,6 +49,12 @@
**Note:** This is a breaking change, but the UX Translator component is still experimental.
- **[BC BREAK]** Refactor `TranslationsDumper` to accept configuration options via `dump()` method parameters, instead of constructor arguments or method calls:
- Removed `$dumpDir` and `$dumpTypeScript` constructor arguments
- Removed `TranslationsDumper::addIncludedDomain()` and `TranslationsDumper::addExcludedDomain()` methods
**Note:** This is a breaking change, but the UX Translator component is still experimental.
- Add configuration `ux_translator.dump_typescript` to enable/disable TypeScript types dumping,
default to `true`. Generating TypeScript types is useful when developing,
but not in production when using the AssetMapper (which does not use these types).

View File

@@ -26,13 +26,15 @@ return static function (ContainerConfigurator $container): void {
->args([
service('translator'),
service('ux.translator.translations_dumper'),
abstract_arg('dump_directory'),
abstract_arg('dump_typescript'),
abstract_arg('included_domains'),
abstract_arg('excluded_domains'),
])
->tag('kernel.cache_warmer')
->set('ux.translator.translations_dumper', TranslationsDumper::class)
->args([
abstract_arg('dump_directory'),
abstract_arg('dump_typescript'),
service('ux.translator.message_parameters.extractor.message_parameters_extractor'),
service('ux.translator.message_parameters.extractor.intl_message_parameters_extractor'),
service('ux.translator.message_parameters.printer.typescript_message_parameters_printer'),

View File

@@ -18,13 +18,23 @@ use Symfony\UX\Translator\TranslationsDumper;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*
* @internal
*
* @experimental
*/
class TranslationsCacheWarmer implements CacheWarmerInterface
{
/**
* @param list<string> $includedDomains
* @param list<string> $excludedDomains
*/
public function __construct(
private TranslatorBagInterface $translatorBag,
private TranslationsDumper $translationsDumper,
private string $dumpDir,
private bool $dumpTypeScript,
private array $includedDomains,
private array $excludedDomains,
) {
}
@@ -36,7 +46,11 @@ class TranslationsCacheWarmer implements CacheWarmerInterface
public function warmUp(string $cacheDir, ?string $buildDir = null): array
{
$this->translationsDumper->dump(
...$this->translatorBag->getCatalogues()
$this->translatorBag->getCatalogues(),
$this->dumpDir,
$this->dumpTypeScript,
$this->includedDomains,
$this->excludedDomains,
);
// No need to preload anything

View File

@@ -35,16 +35,22 @@ class UxTranslatorExtension extends Extension implements PrependExtensionInterfa
$loader = (new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/../config')));
$loader->load('services.php');
$dumperDefinition = $container->getDefinition('ux.translator.translations_dumper');
$dumperDefinition->setArgument(0, $config['dump_directory']);
$dumperDefinition->setArgument(1, $config['dump_typescript']);
$includedDomains = [];
$excludedDomains = [];
if (isset($config['domains'])) {
$method = 'inclusive' === $config['domains']['type'] ? 'addIncludedDomain' : 'addExcludedDomain';
foreach ($config['domains']['elements'] as $domainName) {
$dumperDefinition->addMethodCall($method, [$domainName]);
if ('inclusive' === $config['domains']['type']) {
$includedDomains = $config['domains']['elements'];
} else {
$excludedDomains = $config['domains']['elements'];
}
}
$cacheWarmerDefinition = $container->getDefinition('ux.translator.cache_warmer.translations_cache_warmer');
$cacheWarmerDefinition->setArgument(2, $config['dump_directory']);
$cacheWarmerDefinition->setArgument(3, $config['dump_typescript']);
$cacheWarmerDefinition->setArgument(4, $includedDomains);
$cacheWarmerDefinition->setArgument(5, $excludedDomains);
}
public function prepend(ContainerBuilder $container): void

View File

@@ -30,12 +30,7 @@ use Symfony\UX\Translator\MessageParameters\Printer\TypeScriptMessageParametersP
*/
class TranslationsDumper
{
private array $excludedDomains = [];
private array $includedDomains = [];
public function __construct(
private string $dumpDir,
private bool $dumpTypeScript,
private MessageParametersExtractor $messageParametersExtractor,
private IntlMessageParametersExtractor $intlMessageParametersExtractor,
private TypeScriptMessageParametersPrinter $typeScriptMessageParametersPrinter,
@@ -43,11 +38,25 @@ class TranslationsDumper
) {
}
public function dump(MessageCatalogueInterface ...$catalogues): void
{
$this->filesystem->mkdir($this->dumpDir);
$this->filesystem->remove($fileIndexJs = $this->dumpDir.'/index.js');
$this->filesystem->remove($fileIndexDts = $this->dumpDir.'/index.d.ts');
/**
* @param list<MessageCatalogueInterface> $catalogues
* @param list<Domain> $includedDomains
* @param list<Domain> $excludedDomains
*/
public function dump(
array $catalogues,
string $dumpDir,
bool $dumpTypeScript = true,
array $includedDomains = [],
array $excludedDomains = [],
): void {
if ($includedDomains && $excludedDomains) {
throw new \LogicException('You cannot set both "excluded_domains" and "included_domains" at the same time.');
}
$this->filesystem->mkdir($dumpDir);
$this->filesystem->remove($fileIndexJs = $dumpDir.'/index.js');
$this->filesystem->remove($fileIndexDts = $dumpDir.'/index.d.ts');
$this->filesystem->appendToFile(
$fileIndexJs,
@@ -58,10 +67,10 @@ class TranslationsDumper
export const messages = {
JS,
json_encode($this->getLocaleFallbacks(...$catalogues), \JSON_THROW_ON_ERROR)
json_encode($this->getLocaleFallbacks($catalogues), \JSON_THROW_ON_ERROR)
));
if ($this->dumpTypeScript) {
if ($dumpTypeScript) {
$this->filesystem->appendToFile(
$fileIndexDts,
<<<'TS'
@@ -75,7 +84,7 @@ class TranslationsDumper
);
}
foreach ($this->getTranslations(...$catalogues) as $translationId => $translationsByDomainAndLocale) {
foreach ($this->getTranslations($catalogues, $excludedDomains, $includedDomains) as $translationId => $translationsByDomainAndLocale) {
$translationId = str_replace('"', '\\"', $translationId);
$this->filesystem->appendToFile($fileIndexJs, \sprintf(
' "%s": %s,%s',
@@ -84,7 +93,7 @@ class TranslationsDumper
"\n"
));
if ($this->dumpTypeScript) {
if ($dumpTypeScript) {
$this->filesystem->appendToFile($fileIndexDts, \sprintf(
' "%s": %s;%s',
$translationId,
@@ -96,41 +105,29 @@ class TranslationsDumper
$this->filesystem->appendToFile($fileIndexJs, '};'."\n");
if ($this->dumpTypeScript) {
if ($dumpTypeScript) {
$this->filesystem->appendToFile($fileIndexDts, '};'."\n");
}
}
public function addExcludedDomain(string $domain): void
{
if ($this->includedDomains) {
throw new \LogicException('You cannot set both "excluded_domains" and "included_domains" at the same time.');
}
$this->excludedDomains[] = $domain;
}
public function addIncludedDomain(string $domain): void
{
if ($this->excludedDomains) {
throw new \LogicException('You cannot set both "excluded_domains" and "included_domains" at the same time.');
}
$this->includedDomains[] = $domain;
}
/**
* @param list<MessageCatalogueInterface> $catalogues
* @param list<Domain> $excludedDomains
* @param list<Domain> $includedDomains
*
* @return array<MessageId, array<Domain, array<Locale, string>>>
*/
private function getTranslations(MessageCatalogueInterface ...$catalogues): array
private function getTranslations(array $catalogues, array $excludedDomains, array $includedDomains): array
{
$translations = [];
foreach ($catalogues as $catalogue) {
$locale = $catalogue->getLocale();
foreach ($catalogue->getDomains() as $domain) {
if (\in_array($domain, $this->excludedDomains, true)) {
if (\in_array($domain, $excludedDomains, true)) {
continue;
}
if ($this->includedDomains && !\in_array($domain, $this->includedDomains, true)) {
if ($includedDomains && !\in_array($domain, $includedDomains, true)) {
continue;
}
foreach ($catalogue->all($domain) as $id => $message) {
@@ -185,7 +182,10 @@ class TranslationsDumper
);
}
private function getLocaleFallbacks(MessageCatalogueInterface ...$catalogues): array
/**
* @param list<MessageCatalogueInterface> $catalogues
*/
private function getLocaleFallbacks(array $catalogues): array
{
$localesFallbacks = [];

View File

@@ -42,15 +42,24 @@ final class TranslationsCacheWarmerTest extends TestCase
])
);
$dumpDir = '/tmp/translations';
$dumpTypeScript = true;
$includedDomains = [];
$excludedDomains = [];
$translationsDumperMock = $this->createMock(TranslationsDumper::class);
$translationsDumperMock
->expects($this->once())
->method('dump')
->with(...$translatorBag->getCatalogues());
->with($translatorBag->getCatalogues(), $dumpDir, $dumpTypeScript, $includedDomains, $excludedDomains);
$translationsCacheWarmer = new TranslationsCacheWarmer(
$translatorBag,
$translationsDumperMock
$translationsDumperMock,
$dumpDir,
$dumpTypeScript,
$includedDomains,
$excludedDomains
);
$translationsCacheWarmer->warmUp(self::$cacheDir);

View File

@@ -36,14 +36,15 @@ class TranslationsDumperTest extends TestCase
public function testDump()
{
$translationsDumper = new TranslationsDumper(
self::$translationsDumpDir,
true,
new MessageParametersExtractor(),
new IntlMessageParametersExtractor(),
new TypeScriptMessageParametersPrinter(),
new Filesystem(),
);
$translationsDumper->dump(...self::getMessageCatalogues());
$translationsDumper->dump(
catalogues: self::getMessageCatalogues(),
dumpDir: self::$translationsDumpDir,
);
$this->assertFileExists(self::$translationsDumpDir.'/index.js');
$this->assertFileExists(self::$translationsDumpDir.'/index.d.ts');
@@ -113,14 +114,16 @@ class TranslationsDumperTest extends TestCase
public function testShouldNotDumpTypeScriptTypes()
{
$translationsDumper = new TranslationsDumper(
self::$translationsDumpDir,
false,
new MessageParametersExtractor(),
new IntlMessageParametersExtractor(),
new TypeScriptMessageParametersPrinter(),
new Filesystem(),
);
$translationsDumper->dump(...self::getMessageCatalogues());
$translationsDumper->dump(
catalogues: self::getMessageCatalogues(),
dumpDir: self::$translationsDumpDir,
dumpTypeScript: false,
);
$this->assertFileExists(self::$translationsDumpDir.'/index.js');
$this->assertFileDoesNotExist(self::$translationsDumpDir.'/index.d.ts');
@@ -129,16 +132,17 @@ class TranslationsDumperTest extends TestCase
public function testDumpWithExcludedDomains()
{
$translationsDumper = new TranslationsDumper(
self::$translationsDumpDir,
true,
new MessageParametersExtractor(),
new IntlMessageParametersExtractor(),
new TypeScriptMessageParametersPrinter(),
new Filesystem(),
);
$translationsDumper->addExcludedDomain('foobar');
$translationsDumper->dump(...self::getMessageCatalogues());
$translationsDumper->dump(
catalogues: self::getMessageCatalogues(),
dumpDir: self::$translationsDumpDir,
excludedDomains: ['foobar'],
);
$this->assertFileExists(self::$translationsDumpDir.'/index.js');
$this->assertStringNotContainsString('foobar', file_get_contents(self::$translationsDumpDir.'/index.js'));
@@ -147,16 +151,17 @@ class TranslationsDumperTest extends TestCase
public function testDumpIncludedDomains()
{
$translationsDumper = new TranslationsDumper(
self::$translationsDumpDir,
true,
new MessageParametersExtractor(),
new IntlMessageParametersExtractor(),
new TypeScriptMessageParametersPrinter(),
new Filesystem(),
);
$translationsDumper->addIncludedDomain('messages');
$translationsDumper->dump(...self::getMessageCatalogues());
$translationsDumper->dump(
catalogues: self::getMessageCatalogues(),
dumpDir: self::$translationsDumpDir,
includedDomains: ['messages'],
);
$this->assertFileExists(self::$translationsDumpDir.'/index.js');
$this->assertStringNotContainsString('foobar', file_get_contents(self::$translationsDumpDir.'/index.js'));
@@ -168,16 +173,18 @@ class TranslationsDumperTest extends TestCase
$this->expectExceptionMessage('You cannot set both "excluded_domains" and "included_domains" at the same time.');
$translationsDumper = new TranslationsDumper(
self::$translationsDumpDir,
true,
new MessageParametersExtractor(),
new IntlMessageParametersExtractor(),
new TypeScriptMessageParametersPrinter(),
new Filesystem(),
);
$translationsDumper->addIncludedDomain('foobar');
$translationsDumper->addExcludedDomain('messages');
$translationsDumper->dump(
catalogues: self::getMessageCatalogues(),
dumpDir: self::$translationsDumpDir,
includedDomains: ['foobar'],
excludedDomains: ['messages'],
);
}
public function testSetBothExcludedAndIncludedDomains()
@@ -186,15 +193,17 @@ class TranslationsDumperTest extends TestCase
$this->expectExceptionMessage('You cannot set both "excluded_domains" and "included_domains" at the same time.');
$translationsDumper = new TranslationsDumper(
self::$translationsDumpDir,
true,
new MessageParametersExtractor(),
new IntlMessageParametersExtractor(),
new TypeScriptMessageParametersPrinter(),
new Filesystem(),
);
$translationsDumper->addExcludedDomain('foobar');
$translationsDumper->addIncludedDomain('messages');
$translationsDumper->dump(
catalogues: self::getMessageCatalogues(),
dumpDir: self::$translationsDumpDir,
includedDomains: ['messages'],
excludedDomains: ['foobar'],
);
}
/**