From 741b42156636d3781a2ae2b9b46863403899d30d Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Sat, 6 Dec 2025 02:27:34 +0900 Subject: [PATCH] [Translator] Add option `ux_translator.dump_typescript` to enable/disable TypeScript types generation --- src/Translator/CHANGELOG.md | 4 + src/Translator/config/services.php | 3 +- src/Translator/doc/index.rst | 16 ++++ .../src/DependencyInjection/Configuration.php | 9 ++- .../UxTranslatorExtension.php | 1 + src/Translator/src/TranslationsDumper.php | 44 ++++++---- .../tests/TranslationsDumperTest.php | 81 +++++++++++++++---- 7 files changed, 123 insertions(+), 35 deletions(-) diff --git a/src/Translator/CHANGELOG.md b/src/Translator/CHANGELOG.md index 6cfe1c52b58..fc11ccfe36f 100644 --- a/src/Translator/CHANGELOG.md +++ b/src/Translator/CHANGELOG.md @@ -49,6 +49,10 @@ **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). + ## 2.30 - Ensure compatibility with PHP 8.5 diff --git a/src/Translator/config/services.php b/src/Translator/config/services.php index e5eb6bab50b..691b586a679 100644 --- a/src/Translator/config/services.php +++ b/src/Translator/config/services.php @@ -31,7 +31,8 @@ ->set('ux.translator.translations_dumper', TranslationsDumper::class) ->args([ - null, // Dump directory + 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'), diff --git a/src/Translator/doc/index.rst b/src/Translator/doc/index.rst index 53a49e7d186..35b46433898 100644 --- a/src/Translator/doc/index.rst +++ b/src/Translator/doc/index.rst @@ -106,6 +106,22 @@ including or excluding translation domains in your ``config/packages/ux_translat domains: [foo, bar] # Include only domains 'foo' and 'bar' domains: ['!foo', '!bar'] # Include all domains, except 'foo' and 'bar' +Disabling TypeScript types dump +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, TypeScript types definitions are generated alongside the dumped JavaScript translations. +This provides autocompletion and type-safety when using the ``trans()`` function in your assets. + +Even if they are useful when developing, dumping these TypeScript types is useless in production if you use the +AssetMapper, because these files will never be used. + +You can disable the TypeScript types dump by adding the following configuration: + +.. code-block:: yaml + + when@prod: + ux_translator: + dump_typescript: false Configuring the default locale ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/Translator/src/DependencyInjection/Configuration.php b/src/Translator/src/DependencyInjection/Configuration.php index 5f2e31a3a82..0b75a642fce 100644 --- a/src/Translator/src/DependencyInjection/Configuration.php +++ b/src/Translator/src/DependencyInjection/Configuration.php @@ -28,7 +28,14 @@ public function getConfigTreeBuilder(): TreeBuilder $rootNode = $treeBuilder->getRootNode(); $rootNode ->children() - ->scalarNode('dump_directory')->defaultValue('%kernel.project_dir%/var/translations')->end() + ->scalarNode('dump_directory') + ->info('The directory where translations and TypeScript types are dumped.') + ->defaultValue('%kernel.project_dir%/var/translations') + ->end() + ->booleanNode('dump_typescript') + ->info('Control if TypeScript types should be dumped alongside translations. Can be useful to disable when not using TypeScript (e.g. AssetMapper in production).') + ->defaultTrue() + ->end() ->arrayNode('domains') ->info('List of domains to include/exclude from the generated translations. Prefix with a `!` to exclude a domain.') ->children() diff --git a/src/Translator/src/DependencyInjection/UxTranslatorExtension.php b/src/Translator/src/DependencyInjection/UxTranslatorExtension.php index 3ebfbe518e9..fea927ed6fb 100644 --- a/src/Translator/src/DependencyInjection/UxTranslatorExtension.php +++ b/src/Translator/src/DependencyInjection/UxTranslatorExtension.php @@ -37,6 +37,7 @@ public function load(array $configs, ContainerBuilder $container): void $dumperDefinition = $container->getDefinition('ux.translator.translations_dumper'); $dumperDefinition->setArgument(0, $config['dump_directory']); + $dumperDefinition->setArgument(1, $config['dump_typescript']); if (isset($config['domains'])) { $method = 'inclusive' === $config['domains']['type'] ? 'addIncludedDomain' : 'addExcludedDomain'; diff --git a/src/Translator/src/TranslationsDumper.php b/src/Translator/src/TranslationsDumper.php index e2e9f68ab82..2b09cbfffe9 100644 --- a/src/Translator/src/TranslationsDumper.php +++ b/src/Translator/src/TranslationsDumper.php @@ -35,6 +35,7 @@ class TranslationsDumper public function __construct( private string $dumpDir, + private bool $dumpTypeScript, private MessageParametersExtractor $messageParametersExtractor, private IntlMessageParametersExtractor $intlMessageParametersExtractor, private TypeScriptMessageParametersPrinter $typeScriptMessageParametersPrinter, @@ -54,24 +55,26 @@ public function dump(MessageCatalogueInterface ...$catalogues): void // This file is auto-generated by the Symfony UX Translator. Do not edit it manually. export const localeFallbacks = %s; + export const messages = { JS, json_encode($this->getLocaleFallbacks(...$catalogues), \JSON_THROW_ON_ERROR) )); - $this->filesystem->appendToFile( - $fileIndexDts, - <<<'TS' - // This file is auto-generated by the Symfony UX Translator. Do not edit it manually. - import { Message, NoParametersType, LocaleType } from '@symfony/ux-translator'; + if ($this->dumpTypeScript) { + $this->filesystem->appendToFile( + $fileIndexDts, + <<<'TS' + // This file is auto-generated by the Symfony UX Translator. Do not edit it manually. + import { Message, NoParametersType, LocaleType } from '@symfony/ux-translator'; - export declare const localeFallbacks: Record; + export declare const localeFallbacks: Record; + export declare const messages: { - TS - ); + TS + ); + } - $this->filesystem->appendToFile($fileIndexJs, 'export const messages = {'."\n"); - $this->filesystem->appendToFile($fileIndexDts, 'export declare const messages: {'."\n"); foreach ($this->getTranslations(...$catalogues) as $translationId => $translationsByDomainAndLocale) { $translationId = str_replace('"', '\\"', $translationId); $this->filesystem->appendToFile($fileIndexJs, \sprintf( @@ -80,15 +83,22 @@ public function dump(MessageCatalogueInterface ...$catalogues): void json_encode(['translations' => $translationsByDomainAndLocale], \JSON_THROW_ON_ERROR), "\n" )); - $this->filesystem->appendToFile($fileIndexDts, \sprintf( - ' "%s": %s;%s', - $translationId, - $this->getTranslationsTypeScriptTypeDefinition($translationsByDomainAndLocale), - "\n" - )); + + if ($this->dumpTypeScript) { + $this->filesystem->appendToFile($fileIndexDts, \sprintf( + ' "%s": %s;%s', + $translationId, + $this->getTranslationsTypeScriptTypeDefinition($translationsByDomainAndLocale), + "\n" + )); + } } + $this->filesystem->appendToFile($fileIndexJs, '};'."\n"); - $this->filesystem->appendToFile($fileIndexDts, '};'."\n"); + + if ($this->dumpTypeScript) { + $this->filesystem->appendToFile($fileIndexDts, '};'."\n"); + } } public function addExcludedDomain(string $domain): void diff --git a/src/Translator/tests/TranslationsDumperTest.php b/src/Translator/tests/TranslationsDumperTest.php index 08c71b7334f..9091e672abc 100644 --- a/src/Translator/tests/TranslationsDumperTest.php +++ b/src/Translator/tests/TranslationsDumperTest.php @@ -22,7 +22,6 @@ class TranslationsDumperTest extends TestCase { protected static $translationsDumpDir; - private TranslationsDumper $translationsDumper; public static function setUpBeforeClass(): void { @@ -34,20 +33,17 @@ public static function tearDownAfterClass(): void @rmdir(self::$translationsDumpDir); } - protected function setUp(): void + public function testDump() { - $this->translationsDumper = new TranslationsDumper( + $translationsDumper = new TranslationsDumper( self::$translationsDumpDir, + true, new MessageParametersExtractor(), new IntlMessageParametersExtractor(), new TypeScriptMessageParametersPrinter(), new Filesystem(), ); - } - - public function testDump() - { - $this->translationsDumper->dump(...self::getMessageCatalogues()); + $translationsDumper->dump(...self::getMessageCatalogues()); $this->assertFileExists(self::$translationsDumpDir.'/index.js'); $this->assertFileExists(self::$translationsDumpDir.'/index.d.ts'); @@ -114,10 +110,35 @@ public function testDump() TS); } + public function testShouldNotDumpTypeScriptTypes() + { + $translationsDumper = new TranslationsDumper( + self::$translationsDumpDir, + false, + new MessageParametersExtractor(), + new IntlMessageParametersExtractor(), + new TypeScriptMessageParametersPrinter(), + new Filesystem(), + ); + $translationsDumper->dump(...self::getMessageCatalogues()); + + $this->assertFileExists(self::$translationsDumpDir.'/index.js'); + $this->assertFileDoesNotExist(self::$translationsDumpDir.'/index.d.ts'); + } + public function testDumpWithExcludedDomains() { - $this->translationsDumper->addExcludedDomain('foobar'); - $this->translationsDumper->dump(...$this->getMessageCatalogues()); + $translationsDumper = new TranslationsDumper( + self::$translationsDumpDir, + true, + new MessageParametersExtractor(), + new IntlMessageParametersExtractor(), + new TypeScriptMessageParametersPrinter(), + new Filesystem(), + ); + $translationsDumper->addExcludedDomain('foobar'); + + $translationsDumper->dump(...self::getMessageCatalogues()); $this->assertFileExists(self::$translationsDumpDir.'/index.js'); $this->assertStringNotContainsString('foobar', file_get_contents(self::$translationsDumpDir.'/index.js')); @@ -125,8 +146,17 @@ public function testDumpWithExcludedDomains() public function testDumpIncludedDomains() { - $this->translationsDumper->addIncludedDomain('messages'); - $this->translationsDumper->dump(...$this->getMessageCatalogues()); + $translationsDumper = new TranslationsDumper( + self::$translationsDumpDir, + true, + new MessageParametersExtractor(), + new IntlMessageParametersExtractor(), + new TypeScriptMessageParametersPrinter(), + new Filesystem(), + ); + $translationsDumper->addIncludedDomain('messages'); + + $translationsDumper->dump(...self::getMessageCatalogues()); $this->assertFileExists(self::$translationsDumpDir.'/index.js'); $this->assertStringNotContainsString('foobar', file_get_contents(self::$translationsDumpDir.'/index.js')); @@ -136,16 +166,35 @@ public function testSetBothIncludedAndExcludedDomains() { $this->expectException(\LogicException::class); $this->expectExceptionMessage('You cannot set both "excluded_domains" and "included_domains" at the same time.'); - $this->translationsDumper->addIncludedDomain('foobar'); - $this->translationsDumper->addExcludedDomain('messages'); + + $translationsDumper = new TranslationsDumper( + self::$translationsDumpDir, + true, + new MessageParametersExtractor(), + new IntlMessageParametersExtractor(), + new TypeScriptMessageParametersPrinter(), + new Filesystem(), + ); + + $translationsDumper->addIncludedDomain('foobar'); + $translationsDumper->addExcludedDomain('messages'); } public function testSetBothExcludedAndIncludedDomains() { $this->expectException(\LogicException::class); $this->expectExceptionMessage('You cannot set both "excluded_domains" and "included_domains" at the same time.'); - $this->translationsDumper->addExcludedDomain('foobar'); - $this->translationsDumper->addIncludedDomain('messages'); + + $translationsDumper = new TranslationsDumper( + self::$translationsDumpDir, + true, + new MessageParametersExtractor(), + new IntlMessageParametersExtractor(), + new TypeScriptMessageParametersPrinter(), + new Filesystem(), + ); + $translationsDumper->addExcludedDomain('foobar'); + $translationsDumper->addIncludedDomain('messages'); } /**