[Translator] Add option ux_translator.dump_typescript to enable/disable TypeScript types generation

This commit is contained in:
Hugo Alliaume
2025-12-06 02:27:34 +09:00
parent ad5d349143
commit 4915996a67
7 changed files with 123 additions and 35 deletions

View File

@@ -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

View File

@@ -31,7 +31,8 @@ return static function (ContainerConfigurator $container): void {
->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'),

View File

@@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -28,7 +28,14 @@ class Configuration implements ConfigurationInterface
$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()

View File

@@ -37,6 +37,7 @@ class UxTranslatorExtension extends Extension implements PrependExtensionInterfa
$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';

View File

@@ -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 @@ class TranslationsDumper
// 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<LocaleType, LocaleType>;
export declare const localeFallbacks: Record<LocaleType, LocaleType>;
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 @@ class TranslationsDumper
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

View File

@@ -22,7 +22,6 @@ use Symfony\UX\Translator\TranslationsDumper;
class TranslationsDumperTest extends TestCase
{
protected static $translationsDumpDir;
private TranslationsDumper $translationsDumper;
public static function setUpBeforeClass(): void
{
@@ -34,20 +33,17 @@ class TranslationsDumperTest extends TestCase
@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 @@ class TranslationsDumperTest extends TestCase
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 @@ class TranslationsDumperTest extends TestCase
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 @@ class TranslationsDumperTest extends TestCase
{
$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');
}
/**