From 80cd5bf0c155020f4669a4023d11ddcf9ae99913 Mon Sep 17 00:00:00 2001 From: Franck Matsos Date: Wed, 11 Sep 2024 15:58:02 +0200 Subject: [PATCH 01/16] feat: translations extraction command Introduced a new `translations:extract` command to facilitate extraction and processing of translation strings from layouts. Signed-off-by: Franck Matsos --- .gitignore | 1 + src/Application.php | 1 + src/Command/TranslationsExtract.php | 412 ++++++++++++++++++++++++++++ 3 files changed, 414 insertions(+) create mode 100644 src/Command/TranslationsExtract.php diff --git a/.gitignore b/.gitignore index 8aab437ee..20a1af44d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ vendor/ +.idea/ .vscode/ .phpunit.result.cache .php-cs-fixer.cache diff --git a/src/Application.php b/src/Application.php index 2a068721a..7d8e2f042 100644 --- a/src/Application.php +++ b/src/Application.php @@ -56,6 +56,7 @@ protected function getDefaultCommands(): array new Command\ShowContent(), new Command\ShowConfig(), new Command\ListCommand(), + new Command\TranslationsExtract() ]; if (Util\Plateform::isPhar()) { $commands[] = new Command\SelfUpdate(); diff --git a/src/Command/TranslationsExtract.php b/src/Command/TranslationsExtract.php new file mode 100644 index 000000000..29f17fb29 --- /dev/null +++ b/src/Command/TranslationsExtract.php @@ -0,0 +1,412 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Cecil\Command; + +use Symfony\Bridge\Twig\Extension\TranslationExtension; +use Symfony\Bridge\Twig\Translation\TwigExtractor; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Translation\Catalogue\AbstractOperation; +use Symfony\Component\Translation\Catalogue\MergeOperation; +use Symfony\Component\Translation\Catalogue\OperationInterface; +use Symfony\Component\Translation\Catalogue\TargetOperation; +use Symfony\Component\Translation\Dumper\PoFileDumper; +use Symfony\Component\Translation\Dumper\XliffFileDumper; +use Symfony\Component\Translation\Dumper\YamlFileDumper; +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\MessageCatalogueInterface; +use Symfony\Component\Translation\Reader\TranslationReader; +use Symfony\Component\Translation\Reader\TranslationReaderInterface; +use Symfony\Component\Translation\Writer\TranslationWriter; +use Symfony\Component\Translation\Writer\TranslationWriterInterface; +use Twig\Environment; +use Twig\Loader\FilesystemLoader; + +class TranslationsExtract extends AbstractCommand +{ + private const SORT_ASC = 'asc'; + private const SORT_DESC = 'desc'; + private const SORT_ORDERS = [self::SORT_ASC, self::SORT_DESC]; + private const AVAILABLE_FORMATS = [ + 'xlf12' => ['xlf', '1.2'], + 'xlf20' => ['xlf', '2.0'], + 'po' => ['po'], + 'yaml' => ['yaml'], + ]; + private const ERROR_OPTION_SELECTION = 'You must choose one of --force or --dump-messages option'; + private const ERROR_MISSING_THEME_NAME = 'You must specify a theme name with --name option'; + private const ERROR_WRONG_FORMAT = 'Wrong output format. Supported formats are: %s'; + private const ERROR_WRONG_SORT = 'Wrong sort format. Supported sorts are: %s'; + + private TranslationWriterInterface $writer; + private TranslationReaderInterface $reader; + private TwigExtractor $extractor; + + protected function configure(): void + { + $this + ->setName('translations:extract') + ->setDescription('Extracts translations from layouts') + ->setDefinition([ + new InputArgument('locale', InputArgument::OPTIONAL, 'The locale', 'fr'), + new InputArgument('path', InputArgument::OPTIONAL, 'Use the given path as working directory'), + new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'), + new InputOption('clean', null, InputOption::VALUE_NONE, 'Clean not found messages'), + new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'Specify the domain to extract'), + new InputOption('dump-messages', null, InputOption::VALUE_NONE, 'Should the messages be dumped in the console'), + new InputOption('force', null, InputOption::VALUE_NONE, 'Should the extract be done'), + new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'po'), + new InputOption('is-theme', null, InputOption::VALUE_NONE, 'Use if you want to translate a theme layout'), + new InputOption('name', null, InputOption::VALUE_OPTIONAL, 'The theme name (only works with --is-theme)'), + new InputOption('prefix', null, InputOption::VALUE_OPTIONAL, 'Override the default prefix', '__'), + new InputOption('sort', null, InputOption::VALUE_OPTIONAL, 'Return list of messages sorted alphabetically (only works with --dump-messages)', 'asc'), + ]) + ->setHelp( + <<<'EOF' +The %command.name% command extracts translation strings from your layouts. It can display them or merge +the new ones into the translation files. +When new translation strings are found it can automatically add a prefix to the translation message. + +Example running against default directory: + + php %command.full_name% --dump-messages + php %command.full_name% --force --prefix="new_" en + +You can sort the output with the --sort flag: + + php %command.full_name% --dump-messages --sort=desc fr + php %command.full_name% --dump-messages --sort=asc en path/to/sources + +You can dump a tree-like structure using the yaml format with --as-tree flag: + + php %command.full_name% --force --format=yaml --as-tree=3 fr + php %command.full_name% --force --format=yaml --as-tree=3 en path/to/sources + +You can extract translations from a given theme with --is-theme and --name flags: + + php %command.full_name% --force --is-theme --name=hyde +EOF + ) + ; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->initializeTranslationComponents(); + + $errorIo = $this->io->getErrorStyle(); + + $xliffVersion = '1.2'; + $format = $input->getOption('format'); + + $config = $this->getBuilder()->getConfig(); + $layoutsPath = $config->getSourceDir(); + $translationsPath = $config->getTranslationsPath(); + + try { + [$format, $xliffVersion] = $this->checkOptions($input, $format, $xliffVersion); + } catch (\Exception $exception) { + $errorIo->error($exception->getMessage()); + return self::FAILURE; + } + + if ($input->getOption('is-theme')) { + $layoutsPath = $config->getThemeDirPath($input->getOption('name')); + } + + $this->initializeTwigExtractor($layoutsPath); + + $this->io->title('Translation Messages Extractor and Dumper'); + $this->io->comment(\sprintf('Generating "%s" translation files', $input->getArgument('locale'))); + + $this->io->comment('Parsing templates...'); + $extractedCatalogue = $this->extractMessages( + $input->getArgument('locale'), + $layoutsPath, + $input->getOption('prefix') + ); + + $this->io->comment('Loading translation files...'); + $currentCatalogue = $this->loadCurrentMessages($input->getArgument('locale'), $translationsPath); + + if (null !== $domain = $input->getOption('domain')) { + $currentCatalogue = $this->filterCatalogue($currentCatalogue, $domain); + $extractedCatalogue = $this->filterCatalogue($extractedCatalogue, $domain); + } + + try { + $operation = $this->getOperation($input->getOption('clean'), $currentCatalogue, $extractedCatalogue); + } catch (\Exception $exception) { + $errorIo->error($exception->getMessage()); + return self::SUCCESS; + } + + if ('xlf' === $format) { + $this->io->comment(\sprintf('Xliff output version is %s', $xliffVersion)); + } + + // Show compiled list of messages + if (true === $input->getOption('dump-messages')) { + try { + $this->dumpMessages($operation, $input->getOption('sort')); + } catch (\Exception) { + $errorIo->error(\sprintf(self::ERROR_WRONG_SORT, implode(', ', self::SORT_ORDERS))); + return self::FAILURE; + } + } + + // Save the files + if (true === $input->getOption('force')) { + try { + $this->saveDump( + $operation->getResult(), + $format, + $translationsPath, + $config->getLanguageDefault(), + $xliffVersion, + $input->getOption('as-tree') + ); + } catch (\InvalidArgumentException $exception) { + $this->io->error('Error while updating translations files: ' . $exception->getMessage()); + return self::FAILURE; + } + } + + return self::SUCCESS; + } + + /** + * Checks the provided options and validates the format and xliff version. + * + * @return array An array containing the validated format and xliff version. + * + * @throws \Exception If required options are not provided or if the format is not supported. + */ + private function checkOptions(InputInterface $input, string $format, string $xliffVersion): array + { + if (true !== $input->getOption('force') && true !== $input->getOption('dump-messages')) { + throw new \Exception(self::ERROR_OPTION_SELECTION); + } + + if (true === $input->getOption('is-theme') && null === $input->getOption('name')) { + throw new \Exception(self::ERROR_MISSING_THEME_NAME); + } + + // Get Xliff version + if (\array_key_exists($format, self::AVAILABLE_FORMATS)) { + [$format, $xliffVersion] = self::AVAILABLE_FORMATS[$format]; + } + + // Check format + // @phpstan-ignore-next-line + $supportedFormats = $this->writer->getFormats(); + + if (!\in_array($format, $supportedFormats, true)) { + throw new \Exception( + \sprintf(self::ERROR_WRONG_FORMAT, implode(', ', array_keys(self::AVAILABLE_FORMATS))) + ); + } + + return [$format, $xliffVersion]; + } + + private function initializeTranslationComponents(): void + { + $this->writer = new TranslationWriter(); + $this->writer->addDumper('xlf', new XliffFileDumper()); + $this->writer->addDumper('po', new PoFileDumper()); + $this->writer->addDumper('yaml', new YamlFileDumper()); + + $this->reader = new TranslationReader(); + } + + private function initializeTwigExtractor(string $layoutsPath): void + { + $twig = new Environment(new FilesystemLoader($layoutsPath)); + $twig->addExtension(new TranslationExtension()); + + $this->extractor = new TwigExtractor($twig); + } + + private function extractMessages(string $locale, string $codePath, string $prefix): MessageCatalogue + { + $extractedCatalogue = new MessageCatalogue($locale); + $this->extractor->setPrefix($prefix); + + if (is_dir($codePath) || is_file($codePath)) { + $this->extractor->extract($codePath, $extractedCatalogue); + } + + return $extractedCatalogue; + } + + private function loadCurrentMessages(string $locale, string $translationsPath): MessageCatalogue + { + $currentCatalogue = new MessageCatalogue($locale); + + if (is_dir($translationsPath)) { + $this->reader->read($translationsPath, $currentCatalogue); + } + + return $currentCatalogue; + } + + private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue + { + $filteredCatalogue = new MessageCatalogue($catalogue->getLocale()); + + // extract intl-icu messages only + $intlDomain = $domain . MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + if ($intlMessages = $catalogue->all($intlDomain)) { + $filteredCatalogue->add($intlMessages, $intlDomain); + } + + // extract all messages and subtract intl-icu messages + if ($messages = array_diff($catalogue->all($domain), $intlMessages)) { + $filteredCatalogue->add($messages, $domain); + } + foreach ($catalogue->getResources() as $resource) { + $filteredCatalogue->addResource($resource); + } + + if ($metadata = $catalogue->getMetadata('', $intlDomain)) { + foreach ($metadata as $k => $v) { + $filteredCatalogue->setMetadata($k, $v, $intlDomain); + } + } + + if ($metadata = $catalogue->getMetadata('', $domain)) { + foreach ($metadata as $k => $v) { + $filteredCatalogue->setMetadata($k, $v, $domain); + } + } + + return $filteredCatalogue; + } + + /** + * Retrieves the operation that processes the current and extracted message catalogues. + * + * @throws \Exception If no translation messages are found. + */ + private function getOperation( + ?bool $mustClean, + MessageCatalogue $currentCatalogue, + MessageCatalogue $extractedCatalogue + ): AbstractOperation { + $operation = $this->processCatalogues($mustClean, $currentCatalogue, $extractedCatalogue); + + // Exit if no messages found. + if (!\count($operation->getDomains())) { + throw new \Exception('No translation messages were found.'); + } + + $operation->moveMessagesToIntlDomainsIfPossible('new'); + + return $operation; + } + + private function processCatalogues( + ?bool $mustClean, + MessageCatalogueInterface $currentCatalogue, + MessageCatalogueInterface $extractedCatalogue + ): AbstractOperation { + return $mustClean + ? new TargetOperation($currentCatalogue, $extractedCatalogue) + : new MergeOperation($currentCatalogue, $extractedCatalogue); + } + + private function saveDump( + MessageCatalogueInterface $messageCatalogue, + string $format, + string $translationsPath, + string $defaultLocale, + ?string $xliffVersion = '1.2', + ?bool $asTree = false + ): void { + $this->io->newLine(); + $this->io->comment('Writing files...'); + + $this->writer->write($messageCatalogue, $format, [ + 'path' => $translationsPath, + 'default_locale' => $defaultLocale, + 'xliff_version' => $xliffVersion, + 'as_tree' => $asTree, + 'inline' => $asTree ?? 0 + ]); + + $this->io->success('Translations files have been successfully updated.'); + } + + private function dumpMessages(OperationInterface $operation, ?string $sort): void + { + $messagesCount = 0; + $this->io->newLine(); + + foreach ($operation->getDomains() as $domain) { + $newKeys = array_keys($operation->getNewMessages($domain)); + $allKeys = array_keys($operation->getMessages($domain)); + + $list = array_merge( + array_diff($allKeys, $newKeys), + array_map(fn ($key) => \sprintf('%s', $key), $newKeys), + array_map( + fn ($key) => \sprintf('%s', $key), + array_keys($operation->getObsoleteMessages($domain)) + ) + ); + + $domainMessagesCount = \count($list); + + if ($sort) { + $sort = strtolower($sort); + if (!\in_array($sort, self::SORT_ORDERS, true)) { + throw new \Exception(); + } + + sort($list); // default sort ASC + + if (self::SORT_DESC === $sort) { + rsort($list); + } + } + + $this->io->section( + \sprintf( + 'Messages extracted for domain "%s" (%d message%s)', + $domain, + $domainMessagesCount, + $domainMessagesCount > 1 ? 's' : '' + ) + ); + + $this->io->listing($list); + + $messagesCount += $domainMessagesCount; + } + + $this->io->success( + \sprintf( + '%d message%s successfully extracted.', + $messagesCount, + $messagesCount > 1 ? 's were' : ' was' + ) + ); + } +} From 5b21b58b88c2a17de5236cbc38e9e2810514edc0 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Tue, 4 Feb 2025 17:49:29 +0100 Subject: [PATCH 02/16] Big refactoring :-) --- src/Command/TranslationsExtract.php | 256 +++++++--------------------- 1 file changed, 63 insertions(+), 193 deletions(-) diff --git a/src/Command/TranslationsExtract.php b/src/Command/TranslationsExtract.php index 29f17fb29..6a56d7228 100644 --- a/src/Command/TranslationsExtract.php +++ b/src/Command/TranslationsExtract.php @@ -13,6 +13,8 @@ namespace Cecil\Command; +use Cecil\Exception\RuntimeException; +use Cecil\Renderer\Extension\Core as CoreExtension; use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Bridge\Twig\Translation\TwigExtractor; use Symfony\Component\Console\Input\InputArgument; @@ -20,11 +22,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Translation\Catalogue\AbstractOperation; -use Symfony\Component\Translation\Catalogue\MergeOperation; use Symfony\Component\Translation\Catalogue\OperationInterface; use Symfony\Component\Translation\Catalogue\TargetOperation; use Symfony\Component\Translation\Dumper\PoFileDumper; -use Symfony\Component\Translation\Dumper\XliffFileDumper; use Symfony\Component\Translation\Dumper\YamlFileDumper; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\MessageCatalogueInterface; @@ -32,25 +32,17 @@ use Symfony\Component\Translation\Reader\TranslationReaderInterface; use Symfony\Component\Translation\Writer\TranslationWriter; use Symfony\Component\Translation\Writer\TranslationWriterInterface; +use Symfony\Component\Translation\Loader\PoFileLoader; +use Symfony\Component\Translation\Loader\YamlFileLoader; use Twig\Environment; use Twig\Loader\FilesystemLoader; class TranslationsExtract extends AbstractCommand { - private const SORT_ASC = 'asc'; - private const SORT_DESC = 'desc'; - private const SORT_ORDERS = [self::SORT_ASC, self::SORT_DESC]; private const AVAILABLE_FORMATS = [ - 'xlf12' => ['xlf', '1.2'], - 'xlf20' => ['xlf', '2.0'], 'po' => ['po'], 'yaml' => ['yaml'], ]; - private const ERROR_OPTION_SELECTION = 'You must choose one of --force or --dump-messages option'; - private const ERROR_MISSING_THEME_NAME = 'You must specify a theme name with --name option'; - private const ERROR_WRONG_FORMAT = 'Wrong output format. Supported formats are: %s'; - private const ERROR_WRONG_SORT = 'Wrong sort format. Supported sorts are: %s'; - private TranslationWriterInterface $writer; private TranslationReaderInterface $reader; private TwigExtractor $extractor; @@ -61,186 +53,116 @@ protected function configure(): void ->setName('translations:extract') ->setDescription('Extracts translations from layouts') ->setDefinition([ - new InputArgument('locale', InputArgument::OPTIONAL, 'The locale', 'fr'), new InputArgument('path', InputArgument::OPTIONAL, 'Use the given path as working directory'), - new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'), - new InputOption('clean', null, InputOption::VALUE_NONE, 'Clean not found messages'), - new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'Specify the domain to extract'), - new InputOption('dump-messages', null, InputOption::VALUE_NONE, 'Should the messages be dumped in the console'), - new InputOption('force', null, InputOption::VALUE_NONE, 'Should the extract be done'), + new InputOption('locale', null, InputOption::VALUE_OPTIONAL, 'The locale', 'fr'), + new InputOption('show', null, InputOption::VALUE_NONE, 'Should the messages be displayed in the console'), + new InputOption('save', null, InputOption::VALUE_NONE, 'Should the extract be done'), new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'po'), - new InputOption('is-theme', null, InputOption::VALUE_NONE, 'Use if you want to translate a theme layout'), - new InputOption('name', null, InputOption::VALUE_OPTIONAL, 'The theme name (only works with --is-theme)'), - new InputOption('prefix', null, InputOption::VALUE_OPTIONAL, 'Override the default prefix', '__'), - new InputOption('sort', null, InputOption::VALUE_OPTIONAL, 'Return list of messages sorted alphabetically (only works with --dump-messages)', 'asc'), + new InputOption('theme', null, InputOption::VALUE_OPTIONAL, 'Use if you want to translate a theme layout'), ]) ->setHelp( <<<'EOF' The %command.name% command extracts translation strings from your layouts. It can display them or merge the new ones into the translation files. -When new translation strings are found it can automatically add a prefix to the translation message. - -Example running against default directory: - - php %command.full_name% --dump-messages - php %command.full_name% --force --prefix="new_" en - -You can sort the output with the --sort flag: +When new translation strings are found it automatically add a NEW_ prefix to the translation message. - php %command.full_name% --dump-messages --sort=desc fr - php %command.full_name% --dump-messages --sort=asc en path/to/sources +Example running against working directory: -You can dump a tree-like structure using the yaml format with --as-tree flag: + php %command.full_name% --show + php %command.full_name% --save --locale=en - php %command.full_name% --force --format=yaml --as-tree=3 fr - php %command.full_name% --force --format=yaml --as-tree=3 en path/to/sources +You can extract translations from a given theme with --theme option: -You can extract translations from a given theme with --is-theme and --name flags: - - php %command.full_name% --force --is-theme --name=hyde + php %command.full_name% --theme=hyde EOF ) ; } - /** - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ protected function execute(InputInterface $input, OutputInterface $output): int { - $this->initializeTranslationComponents(); - - $errorIo = $this->io->getErrorStyle(); - - $xliffVersion = '1.2'; $format = $input->getOption('format'); + $domain = 'messages'; + + if (true !== $input->getOption('save') && true !== $input->getOption('show')) { + throw new RuntimeException('You must choose to display (`--show`) or save (`--save`) the translations'); + } $config = $this->getBuilder()->getConfig(); - $layoutsPath = $config->getSourceDir(); + $layoutsPath = $config->getLayoutsPath(); $translationsPath = $config->getTranslationsPath(); - try { - [$format, $xliffVersion] = $this->checkOptions($input, $format, $xliffVersion); - } catch (\Exception $exception) { - $errorIo->error($exception->getMessage()); - return self::FAILURE; + $this->initializeTranslationComponents(); + + $supportedFormats = $this->writer->getFormats(); + + if (!\in_array($format, $supportedFormats, true)) { + throw new RuntimeException(\sprintf('Supported formats are: %s', implode(', ', array_keys(self::AVAILABLE_FORMATS)))); } - if ($input->getOption('is-theme')) { - $layoutsPath = $config->getThemeDirPath($input->getOption('name')); + if ($input->getOption('theme')) { + $layoutsPath = $config->getThemeDirPath($input->getOption('theme')); } $this->initializeTwigExtractor($layoutsPath); - $this->io->title('Translation Messages Extractor and Dumper'); - $this->io->comment(\sprintf('Generating "%s" translation files', $input->getArgument('locale'))); - - $this->io->comment('Parsing templates...'); - $extractedCatalogue = $this->extractMessages( - $input->getArgument('locale'), - $layoutsPath, - $input->getOption('prefix') - ); - - $this->io->comment('Loading translation files...'); - $currentCatalogue = $this->loadCurrentMessages($input->getArgument('locale'), $translationsPath); - - if (null !== $domain = $input->getOption('domain')) { - $currentCatalogue = $this->filterCatalogue($currentCatalogue, $domain); - $extractedCatalogue = $this->filterCatalogue($extractedCatalogue, $domain); - } + $this->io->writeln(\sprintf('Generating "%s" translation files', $input->getOption('locale'))); + $this->io->writeln('Parsing templates...'); + $extractedCatalogue = $this->extractMessages($input->getOption('locale'), $layoutsPath, 'NEW_'); + $this->io->writeln('Loading translation files...'); + $currentCatalogue = $this->loadCurrentMessages($input->getOption('locale'), $translationsPath); + $currentCatalogue = $this->filterCatalogue($currentCatalogue, $domain); + $extractedCatalogue = $this->filterCatalogue($extractedCatalogue, $domain); try { - $operation = $this->getOperation($input->getOption('clean'), $currentCatalogue, $extractedCatalogue); + $operation = $this->getOperation($currentCatalogue, $extractedCatalogue); } catch (\Exception $exception) { - $errorIo->error($exception->getMessage()); - return self::SUCCESS; + throw new RuntimeException($exception->getMessage()); } - if ('xlf' === $format) { - $this->io->comment(\sprintf('Xliff output version is %s', $xliffVersion)); - } - - // Show compiled list of messages - if (true === $input->getOption('dump-messages')) { + // show compiled list of messages + if (true === $input->getOption('show')) { try { - $this->dumpMessages($operation, $input->getOption('sort')); - } catch (\Exception) { - $errorIo->error(\sprintf(self::ERROR_WRONG_SORT, implode(', ', self::SORT_ORDERS))); - return self::FAILURE; + $this->dumpMessages($operation); + } catch (\Exception $e) { + throw new RuntimeException('Error while displaying messages: ' . $e->getMessage()); } } - // Save the files - if (true === $input->getOption('force')) { + // save the files + if (true === $input->getOption('save')) { try { $this->saveDump( $operation->getResult(), $format, $translationsPath, - $config->getLanguageDefault(), - $xliffVersion, - $input->getOption('as-tree') + $config->getLanguageDefault() ); - } catch (\InvalidArgumentException $exception) { - $this->io->error('Error while updating translations files: ' . $exception->getMessage()); - return self::FAILURE; + } catch (\InvalidArgumentException $e) { + throw new RuntimeException('Error while saving translations files: ' . $e->getMessage()); } } - return self::SUCCESS; - } - - /** - * Checks the provided options and validates the format and xliff version. - * - * @return array An array containing the validated format and xliff version. - * - * @throws \Exception If required options are not provided or if the format is not supported. - */ - private function checkOptions(InputInterface $input, string $format, string $xliffVersion): array - { - if (true !== $input->getOption('force') && true !== $input->getOption('dump-messages')) { - throw new \Exception(self::ERROR_OPTION_SELECTION); - } - - if (true === $input->getOption('is-theme') && null === $input->getOption('name')) { - throw new \Exception(self::ERROR_MISSING_THEME_NAME); - } - - // Get Xliff version - if (\array_key_exists($format, self::AVAILABLE_FORMATS)) { - [$format, $xliffVersion] = self::AVAILABLE_FORMATS[$format]; - } - - // Check format - // @phpstan-ignore-next-line - $supportedFormats = $this->writer->getFormats(); - - if (!\in_array($format, $supportedFormats, true)) { - throw new \Exception( - \sprintf(self::ERROR_WRONG_FORMAT, implode(', ', array_keys(self::AVAILABLE_FORMATS))) - ); - } - - return [$format, $xliffVersion]; + return 0; } private function initializeTranslationComponents(): void { + // readers + $this->reader = new TranslationReader(); + $this->reader->addLoader('po', new PoFileLoader()); + $this->reader->addLoader('yaml', new YamlFileLoader()); + // writers $this->writer = new TranslationWriter(); - $this->writer->addDumper('xlf', new XliffFileDumper()); $this->writer->addDumper('po', new PoFileDumper()); $this->writer->addDumper('yaml', new YamlFileDumper()); - - $this->reader = new TranslationReader(); } private function initializeTwigExtractor(string $layoutsPath): void { $twig = new Environment(new FilesystemLoader($layoutsPath)); $twig->addExtension(new TranslationExtension()); - + $twig->addExtension(new CoreExtension($this->getBuilder())); $this->extractor = new TwigExtractor($twig); } @@ -248,7 +170,6 @@ private function extractMessages(string $locale, string $codePath, string $prefi { $extractedCatalogue = new MessageCatalogue($locale); $this->extractor->setPrefix($prefix); - if (is_dir($codePath) || is_file($codePath)) { $this->extractor->extract($codePath, $extractedCatalogue); } @@ -259,7 +180,6 @@ private function extractMessages(string $locale, string $codePath, string $prefi private function loadCurrentMessages(string $locale, string $translationsPath): MessageCatalogue { $currentCatalogue = new MessageCatalogue($locale); - if (is_dir($translationsPath)) { $this->reader->read($translationsPath, $currentCatalogue); } @@ -305,64 +225,38 @@ private function filterCatalogue(MessageCatalogue $catalogue, string $domain): M * * @throws \Exception If no translation messages are found. */ - private function getOperation( - ?bool $mustClean, - MessageCatalogue $currentCatalogue, - MessageCatalogue $extractedCatalogue - ): AbstractOperation { - $operation = $this->processCatalogues($mustClean, $currentCatalogue, $extractedCatalogue); - - // Exit if no messages found. + private function getOperation(MessageCatalogue $currentCatalogue, MessageCatalogue $extractedCatalogue): AbstractOperation { + $operation = $this->processCatalogues($currentCatalogue, $extractedCatalogue); if (!\count($operation->getDomains())) { - throw new \Exception('No translation messages were found.'); + throw new RuntimeException('No translation messages were found.'); } - $operation->moveMessagesToIntlDomainsIfPossible('new'); - return $operation; } - private function processCatalogues( - ?bool $mustClean, - MessageCatalogueInterface $currentCatalogue, - MessageCatalogueInterface $extractedCatalogue - ): AbstractOperation { - return $mustClean - ? new TargetOperation($currentCatalogue, $extractedCatalogue) - : new MergeOperation($currentCatalogue, $extractedCatalogue); + private function processCatalogues(MessageCatalogueInterface $currentCatalogue, MessageCatalogueInterface $extractedCatalogue): AbstractOperation { + return new TargetOperation($currentCatalogue, $extractedCatalogue); } - private function saveDump( - MessageCatalogueInterface $messageCatalogue, - string $format, - string $translationsPath, - string $defaultLocale, - ?string $xliffVersion = '1.2', - ?bool $asTree = false - ): void { + private function saveDump(MessageCatalogueInterface $messageCatalogue, string $format, string $translationsPath, string $defaultLocale): void { $this->io->newLine(); - $this->io->comment('Writing files...'); + $this->io->writeln('Writing files...'); $this->writer->write($messageCatalogue, $format, [ 'path' => $translationsPath, 'default_locale' => $defaultLocale, - 'xliff_version' => $xliffVersion, - 'as_tree' => $asTree, - 'inline' => $asTree ?? 0 ]); $this->io->success('Translations files have been successfully updated.'); } - private function dumpMessages(OperationInterface $operation, ?string $sort): void + private function dumpMessages(OperationInterface $operation): void { $messagesCount = 0; $this->io->newLine(); - foreach ($operation->getDomains() as $domain) { $newKeys = array_keys($operation->getNewMessages($domain)); $allKeys = array_keys($operation->getMessages($domain)); - $list = array_merge( array_diff($allKeys, $newKeys), array_map(fn ($key) => \sprintf('%s', $key), $newKeys), @@ -371,33 +265,9 @@ private function dumpMessages(OperationInterface $operation, ?string $sort): voi array_keys($operation->getObsoleteMessages($domain)) ) ); - $domainMessagesCount = \count($list); - - if ($sort) { - $sort = strtolower($sort); - if (!\in_array($sort, self::SORT_ORDERS, true)) { - throw new \Exception(); - } - - sort($list); // default sort ASC - - if (self::SORT_DESC === $sort) { - rsort($list); - } - } - - $this->io->section( - \sprintf( - 'Messages extracted for domain "%s" (%d message%s)', - $domain, - $domainMessagesCount, - $domainMessagesCount > 1 ? 's' : '' - ) - ); - + sort($list); // default sort ASC $this->io->listing($list); - $messagesCount += $domainMessagesCount; } From 024e5396a18a9740b0c56cbe17a196e1bf0d9538 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Tue, 4 Feb 2025 17:52:55 +0100 Subject: [PATCH 03/16] refactor: code quality --- src/Command/TranslationsExtract.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Command/TranslationsExtract.php b/src/Command/TranslationsExtract.php index 6a56d7228..bd9d775f6 100644 --- a/src/Command/TranslationsExtract.php +++ b/src/Command/TranslationsExtract.php @@ -94,6 +94,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->initializeTranslationComponents(); + // @phpstan-ignore-next-line $supportedFormats = $this->writer->getFormats(); if (!\in_array($format, $supportedFormats, true)) { @@ -225,7 +226,8 @@ private function filterCatalogue(MessageCatalogue $catalogue, string $domain): M * * @throws \Exception If no translation messages are found. */ - private function getOperation(MessageCatalogue $currentCatalogue, MessageCatalogue $extractedCatalogue): AbstractOperation { + private function getOperation(MessageCatalogue $currentCatalogue, MessageCatalogue $extractedCatalogue): AbstractOperation + { $operation = $this->processCatalogues($currentCatalogue, $extractedCatalogue); if (!\count($operation->getDomains())) { throw new RuntimeException('No translation messages were found.'); @@ -234,11 +236,13 @@ private function getOperation(MessageCatalogue $currentCatalogue, MessageCatalog return $operation; } - private function processCatalogues(MessageCatalogueInterface $currentCatalogue, MessageCatalogueInterface $extractedCatalogue): AbstractOperation { + private function processCatalogues(MessageCatalogueInterface $currentCatalogue, MessageCatalogueInterface $extractedCatalogue): AbstractOperation + { return new TargetOperation($currentCatalogue, $extractedCatalogue); } - private function saveDump(MessageCatalogueInterface $messageCatalogue, string $format, string $translationsPath, string $defaultLocale): void { + private function saveDump(MessageCatalogueInterface $messageCatalogue, string $format, string $translationsPath, string $defaultLocale): void + { $this->io->newLine(); $this->io->writeln('Writing files...'); From dd32ace30765cf8931e168565610d184ae75bd4f Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Tue, 4 Feb 2025 23:22:41 +0100 Subject: [PATCH 04/16] refactor: code quality --- src/Command/TranslationsExtract.php | 116 ++++++++-------------------- 1 file changed, 31 insertions(+), 85 deletions(-) diff --git a/src/Command/TranslationsExtract.php b/src/Command/TranslationsExtract.php index bd9d775f6..cedd86f04 100644 --- a/src/Command/TranslationsExtract.php +++ b/src/Command/TranslationsExtract.php @@ -62,8 +62,8 @@ protected function configure(): void ]) ->setHelp( <<<'EOF' -The %command.name% command extracts translation strings from your layouts. It can display them or merge -the new ones into the translation files. +The %command.name% command extracts translation strings from your layouts. +It can display them or merge the new ones into the translation file. When new translation strings are found it automatically add a NEW_ prefix to the translation message. Example running against working directory: @@ -73,7 +73,7 @@ protected function configure(): void You can extract translations from a given theme with --theme option: - php %command.full_name% --theme=hyde + php %command.full_name% --show --save --theme=hyde EOF ) ; @@ -81,25 +81,15 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - $format = $input->getOption('format'); - $domain = 'messages'; - - if (true !== $input->getOption('save') && true !== $input->getOption('show')) { - throw new RuntimeException('You must choose to display (`--show`) or save (`--save`) the translations'); - } - $config = $this->getBuilder()->getConfig(); $layoutsPath = $config->getLayoutsPath(); $translationsPath = $config->getTranslationsPath(); - $this->initializeTranslationComponents(); + $format = $input->getOption('format'); - // @phpstan-ignore-next-line - $supportedFormats = $this->writer->getFormats(); + $this->initializeTranslationComponents(); - if (!\in_array($format, $supportedFormats, true)) { - throw new RuntimeException(\sprintf('Supported formats are: %s', implode(', ', array_keys(self::AVAILABLE_FORMATS)))); - } + $this->checkOptions($input, $format); if ($input->getOption('theme')) { $layoutsPath = $config->getThemeDirPath($input->getOption('theme')); @@ -107,18 +97,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->initializeTwigExtractor($layoutsPath); - $this->io->writeln(\sprintf('Generating "%s" translation files', $input->getOption('locale'))); - $this->io->writeln('Parsing templates...'); + $output->writeln(\sprintf('Generating "%s" translation file', $input->getOption('locale'))); + + $output->writeln('Parsing templates...'); $extractedCatalogue = $this->extractMessages($input->getOption('locale'), $layoutsPath, 'NEW_'); - $this->io->writeln('Loading translation files...'); + + $output->writeln('Loading translation file...'); $currentCatalogue = $this->loadCurrentMessages($input->getOption('locale'), $translationsPath); - $currentCatalogue = $this->filterCatalogue($currentCatalogue, $domain); - $extractedCatalogue = $this->filterCatalogue($extractedCatalogue, $domain); try { - $operation = $this->getOperation($currentCatalogue, $extractedCatalogue); - } catch (\Exception $exception) { - throw new RuntimeException($exception->getMessage()); + $operation = new TargetOperation($currentCatalogue, $extractedCatalogue); + } catch (\Exception $e) { + throw new RuntimeException($e->getMessage()); } // show compiled list of messages @@ -130,7 +120,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - // save the files + // save the file if (true === $input->getOption('save')) { try { $this->saveDump( @@ -140,13 +130,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int $config->getLanguageDefault() ); } catch (\InvalidArgumentException $e) { - throw new RuntimeException('Error while saving translations files: ' . $e->getMessage()); + throw new RuntimeException('Error while saving translation file: ' . $e->getMessage()); } } return 0; } + private function checkOptions(InputInterface $input, $format): void + { + if (true !== $input->getOption('save') && true !== $input->getOption('show')) { + throw new RuntimeException('You must choose to display (`--show`) and/or save (`--save`) the translations'); + } + + // @phpstan-ignore-next-line + if (!\in_array($format, $this->writer->getFormats(), true)) { + throw new RuntimeException(\sprintf('Supported formats are: %s', implode(', ', array_keys(self::AVAILABLE_FORMATS)))); + } + } + private function initializeTranslationComponents(): void { // readers @@ -188,70 +190,14 @@ private function loadCurrentMessages(string $locale, string $translationsPath): return $currentCatalogue; } - private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue - { - $filteredCatalogue = new MessageCatalogue($catalogue->getLocale()); - - // extract intl-icu messages only - $intlDomain = $domain . MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; - if ($intlMessages = $catalogue->all($intlDomain)) { - $filteredCatalogue->add($intlMessages, $intlDomain); - } - - // extract all messages and subtract intl-icu messages - if ($messages = array_diff($catalogue->all($domain), $intlMessages)) { - $filteredCatalogue->add($messages, $domain); - } - foreach ($catalogue->getResources() as $resource) { - $filteredCatalogue->addResource($resource); - } - - if ($metadata = $catalogue->getMetadata('', $intlDomain)) { - foreach ($metadata as $k => $v) { - $filteredCatalogue->setMetadata($k, $v, $intlDomain); - } - } - - if ($metadata = $catalogue->getMetadata('', $domain)) { - foreach ($metadata as $k => $v) { - $filteredCatalogue->setMetadata($k, $v, $domain); - } - } - - return $filteredCatalogue; - } - - /** - * Retrieves the operation that processes the current and extracted message catalogues. - * - * @throws \Exception If no translation messages are found. - */ - private function getOperation(MessageCatalogue $currentCatalogue, MessageCatalogue $extractedCatalogue): AbstractOperation - { - $operation = $this->processCatalogues($currentCatalogue, $extractedCatalogue); - if (!\count($operation->getDomains())) { - throw new RuntimeException('No translation messages were found.'); - } - - return $operation; - } - - private function processCatalogues(MessageCatalogueInterface $currentCatalogue, MessageCatalogueInterface $extractedCatalogue): AbstractOperation - { - return new TargetOperation($currentCatalogue, $extractedCatalogue); - } - private function saveDump(MessageCatalogueInterface $messageCatalogue, string $format, string $translationsPath, string $defaultLocale): void { - $this->io->newLine(); - $this->io->writeln('Writing files...'); - + $this->io->writeln('Writing file...'); $this->writer->write($messageCatalogue, $format, [ 'path' => $translationsPath, 'default_locale' => $defaultLocale, ]); - - $this->io->success('Translations files have been successfully updated.'); + $this->io->success('Translation file have been successfully updated.'); } private function dumpMessages(OperationInterface $operation): void @@ -270,7 +216,7 @@ private function dumpMessages(OperationInterface $operation): void ) ); $domainMessagesCount = \count($list); - sort($list); // default sort ASC + sort($list); $this->io->listing($list); $messagesCount += $domainMessagesCount; } From 093529d6522fb9c9eb67df9a36b7a11c45b1ede7 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Tue, 4 Feb 2025 23:27:26 +0100 Subject: [PATCH 05/16] refactor: cleaning code again --- src/Command/TranslationsExtract.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Command/TranslationsExtract.php b/src/Command/TranslationsExtract.php index cedd86f04..2569692b2 100644 --- a/src/Command/TranslationsExtract.php +++ b/src/Command/TranslationsExtract.php @@ -21,7 +21,6 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Translation\Catalogue\AbstractOperation; use Symfony\Component\Translation\Catalogue\OperationInterface; use Symfony\Component\Translation\Catalogue\TargetOperation; use Symfony\Component\Translation\Dumper\PoFileDumper; @@ -39,10 +38,6 @@ class TranslationsExtract extends AbstractCommand { - private const AVAILABLE_FORMATS = [ - 'po' => ['po'], - 'yaml' => ['yaml'], - ]; private TranslationWriterInterface $writer; private TranslationReaderInterface $reader; private TwigExtractor $extractor; @@ -145,7 +140,7 @@ private function checkOptions(InputInterface $input, $format): void // @phpstan-ignore-next-line if (!\in_array($format, $this->writer->getFormats(), true)) { - throw new RuntimeException(\sprintf('Supported formats are: %s', implode(', ', array_keys(self::AVAILABLE_FORMATS)))); + throw new RuntimeException(\sprintf('Supported formats are: %s', implode(', ', $this->writer->getFormats()))); } } From 4f97f8cdcdf7a42f219e6a6ae7efbbd27f9b8ae5 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Tue, 4 Feb 2025 23:41:20 +0100 Subject: [PATCH 06/16] refactor: move command to `util` --- src/Application.php | 2 +- .../{TranslationsExtract.php => UtilTranslationsExtract.php} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/Command/{TranslationsExtract.php => UtilTranslationsExtract.php} (98%) diff --git a/src/Application.php b/src/Application.php index 7d8e2f042..f588689d1 100644 --- a/src/Application.php +++ b/src/Application.php @@ -56,7 +56,7 @@ protected function getDefaultCommands(): array new Command\ShowContent(), new Command\ShowConfig(), new Command\ListCommand(), - new Command\TranslationsExtract() + new Command\UtilTranslationsExtract() ]; if (Util\Plateform::isPhar()) { $commands[] = new Command\SelfUpdate(); diff --git a/src/Command/TranslationsExtract.php b/src/Command/UtilTranslationsExtract.php similarity index 98% rename from src/Command/TranslationsExtract.php rename to src/Command/UtilTranslationsExtract.php index 2569692b2..30936ee81 100644 --- a/src/Command/TranslationsExtract.php +++ b/src/Command/UtilTranslationsExtract.php @@ -36,7 +36,7 @@ use Twig\Environment; use Twig\Loader\FilesystemLoader; -class TranslationsExtract extends AbstractCommand +class UtilTranslationsExtract extends AbstractCommand { private TranslationWriterInterface $writer; private TranslationReaderInterface $reader; @@ -45,7 +45,7 @@ class TranslationsExtract extends AbstractCommand protected function configure(): void { $this - ->setName('translations:extract') + ->setName('util:translations:extract') ->setDescription('Extracts translations from layouts') ->setDefinition([ new InputArgument('path', InputArgument::OPTIONAL, 'Use the given path as working directory'), From 00fbc4fce95387b73de208c319e5a7593c944e45 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Tue, 4 Feb 2025 23:59:26 +0100 Subject: [PATCH 07/16] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 20a1af44d..8aab437ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ vendor/ -.idea/ .vscode/ .phpunit.result.cache .php-cs-fixer.cache From c7e8f10abdf5cea9cfaa8d6202335fbc44e2fecc Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Wed, 5 Feb 2025 01:13:36 +0100 Subject: [PATCH 08/16] Update UtilTranslationsExtract.php --- src/Command/UtilTranslationsExtract.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Command/UtilTranslationsExtract.php b/src/Command/UtilTranslationsExtract.php index 30936ee81..11e562bce 100644 --- a/src/Command/UtilTranslationsExtract.php +++ b/src/Command/UtilTranslationsExtract.php @@ -28,9 +28,7 @@ use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\MessageCatalogueInterface; use Symfony\Component\Translation\Reader\TranslationReader; -use Symfony\Component\Translation\Reader\TranslationReaderInterface; use Symfony\Component\Translation\Writer\TranslationWriter; -use Symfony\Component\Translation\Writer\TranslationWriterInterface; use Symfony\Component\Translation\Loader\PoFileLoader; use Symfony\Component\Translation\Loader\YamlFileLoader; use Twig\Environment; @@ -38,8 +36,8 @@ class UtilTranslationsExtract extends AbstractCommand { - private TranslationWriterInterface $writer; - private TranslationReaderInterface $reader; + private TranslationWriter $writer; + private TranslationReader $reader; private TwigExtractor $extractor; protected function configure(): void @@ -137,8 +135,6 @@ private function checkOptions(InputInterface $input, $format): void if (true !== $input->getOption('save') && true !== $input->getOption('show')) { throw new RuntimeException('You must choose to display (`--show`) and/or save (`--save`) the translations'); } - - // @phpstan-ignore-next-line if (!\in_array($format, $this->writer->getFormats(), true)) { throw new RuntimeException(\sprintf('Supported formats are: %s', implode(', ', $this->writer->getFormats()))); } From bf14f1993ea2a0fb5239a3c6e17e3179da889501 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Wed, 5 Feb 2025 01:38:06 +0100 Subject: [PATCH 09/16] fix: merge theme translations --- src/Command/UtilTranslationsExtract.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Command/UtilTranslationsExtract.php b/src/Command/UtilTranslationsExtract.php index 11e562bce..d05413f3a 100644 --- a/src/Command/UtilTranslationsExtract.php +++ b/src/Command/UtilTranslationsExtract.php @@ -22,6 +22,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Translation\Catalogue\OperationInterface; +use Symfony\Component\Translation\Catalogue\MergeOperation; use Symfony\Component\Translation\Catalogue\TargetOperation; use Symfony\Component\Translation\Dumper\PoFileDumper; use Symfony\Component\Translation\Dumper\YamlFileDumper; @@ -99,7 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $currentCatalogue = $this->loadCurrentMessages($input->getOption('locale'), $translationsPath); try { - $operation = new TargetOperation($currentCatalogue, $extractedCatalogue); + $operation = $input->getOption('theme') ? new MergeOperation($currentCatalogue, $extractedCatalogue) : new TargetOperation($currentCatalogue, $extractedCatalogue); } catch (\Exception $e) { throw new RuntimeException($e->getMessage()); } From fc4610547d30b2159b9026db6d494760247aa2a0 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Wed, 5 Feb 2025 02:39:33 +0100 Subject: [PATCH 10/16] Update UtilTranslationsExtract.php --- src/Command/UtilTranslationsExtract.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Command/UtilTranslationsExtract.php b/src/Command/UtilTranslationsExtract.php index d05413f3a..a9ca7c5b7 100644 --- a/src/Command/UtilTranslationsExtract.php +++ b/src/Command/UtilTranslationsExtract.php @@ -52,7 +52,7 @@ protected function configure(): void new InputOption('show', null, InputOption::VALUE_NONE, 'Should the messages be displayed in the console'), new InputOption('save', null, InputOption::VALUE_NONE, 'Should the extract be done'), new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'po'), - new InputOption('theme', null, InputOption::VALUE_OPTIONAL, 'Use if you want to translate a theme layout'), + new InputOption('theme', null, InputOption::VALUE_OPTIONAL, 'Use if you want to translate a theme layouts'), ]) ->setHelp( <<<'EOF' @@ -67,7 +67,7 @@ protected function configure(): void You can extract translations from a given theme with --theme option: - php %command.full_name% --show --save --theme=hyde + php %command.full_name% --show --theme=hyde EOF ) ; From f42c9af79b80792ea753f40ac9bd69dce46712eb Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Wed, 5 Feb 2025 13:03:53 +0100 Subject: [PATCH 11/16] refactor: better theme support --- src/Command/UtilTranslationsExtract.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Command/UtilTranslationsExtract.php b/src/Command/UtilTranslationsExtract.php index a9ca7c5b7..503dea855 100644 --- a/src/Command/UtilTranslationsExtract.php +++ b/src/Command/UtilTranslationsExtract.php @@ -52,7 +52,7 @@ protected function configure(): void new InputOption('show', null, InputOption::VALUE_NONE, 'Should the messages be displayed in the console'), new InputOption('save', null, InputOption::VALUE_NONE, 'Should the extract be done'), new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'po'), - new InputOption('theme', null, InputOption::VALUE_OPTIONAL, 'Use if you want to translate a theme layouts'), + new InputOption('theme', null, InputOption::VALUE_OPTIONAL, 'Use if you want to translate a theme layouts too'), ]) ->setHelp( <<<'EOF' @@ -65,7 +65,7 @@ protected function configure(): void php %command.full_name% --show php %command.full_name% --save --locale=en -You can extract translations from a given theme with --theme option: +You can extract, and merge, translations from a given theme with --theme option: php %command.full_name% --show --theme=hyde EOF @@ -86,7 +86,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->checkOptions($input, $format); if ($input->getOption('theme')) { - $layoutsPath = $config->getThemeDirPath($input->getOption('theme')); + $layoutsPath = [$layoutsPath, $config->getThemeDirPath($input->getOption('theme'))]; } $this->initializeTwigExtractor($layoutsPath); @@ -153,7 +153,7 @@ private function initializeTranslationComponents(): void $this->writer->addDumper('yaml', new YamlFileDumper()); } - private function initializeTwigExtractor(string $layoutsPath): void + private function initializeTwigExtractor($layoutsPath = []): void { $twig = new Environment(new FilesystemLoader($layoutsPath)); $twig->addExtension(new TranslationExtension()); @@ -161,12 +161,13 @@ private function initializeTwigExtractor(string $layoutsPath): void $this->extractor = new TwigExtractor($twig); } - private function extractMessages(string $locale, string $codePath, string $prefix): MessageCatalogue + private function extractMessages(string $locale, $layoutsPath = [], string $prefix): MessageCatalogue { $extractedCatalogue = new MessageCatalogue($locale); $this->extractor->setPrefix($prefix); - if (is_dir($codePath) || is_file($codePath)) { - $this->extractor->extract($codePath, $extractedCatalogue); + $layoutsPath = is_array($layoutsPath) ? $layoutsPath : [$layoutsPath]; + foreach ($layoutsPath as $path) { + $this->extractor->extract($path, $extractedCatalogue); } return $extractedCatalogue; From a7209b9f63be1afe0c9f629caf61ac28ca4cc7ed Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Wed, 5 Feb 2025 13:10:49 +0100 Subject: [PATCH 12/16] refactor: use Cecil's Twig instance --- src/Command/UtilTranslationsExtract.php | 4 +--- src/Renderer/Twig.php | 8 ++++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Command/UtilTranslationsExtract.php b/src/Command/UtilTranslationsExtract.php index 503dea855..907511a9f 100644 --- a/src/Command/UtilTranslationsExtract.php +++ b/src/Command/UtilTranslationsExtract.php @@ -155,9 +155,7 @@ private function initializeTranslationComponents(): void private function initializeTwigExtractor($layoutsPath = []): void { - $twig = new Environment(new FilesystemLoader($layoutsPath)); - $twig->addExtension(new TranslationExtension()); - $twig->addExtension(new CoreExtension($this->getBuilder())); + $twig = (new \Cecil\Renderer\Twig($this->getBuilder(), $layoutsPath))->getTwig(); $this->extractor = new TwigExtractor($twig); } diff --git a/src/Renderer/Twig.php b/src/Renderer/Twig.php index 5431894f7..cd829e773 100644 --- a/src/Renderer/Twig.php +++ b/src/Renderer/Twig.php @@ -198,4 +198,12 @@ public function getDebugProfile(): ?\Twig\Profiler\Profile { return $this->profile; } + + /** + * Returns the Twig instance. + */ + public function getTwig(): \Twig\Environment + { + return $this->twig; + } } From 8efa22e9fb594ac71de6e6426308e360f3098faf Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Wed, 5 Feb 2025 13:57:56 +0100 Subject: [PATCH 13/16] refactor: code quality --- src/Command/UtilTranslationsExtract.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Command/UtilTranslationsExtract.php b/src/Command/UtilTranslationsExtract.php index 907511a9f..f678a7c26 100644 --- a/src/Command/UtilTranslationsExtract.php +++ b/src/Command/UtilTranslationsExtract.php @@ -159,11 +159,11 @@ private function initializeTwigExtractor($layoutsPath = []): void $this->extractor = new TwigExtractor($twig); } - private function extractMessages(string $locale, $layoutsPath = [], string $prefix): MessageCatalogue + private function extractMessages(string $locale, $layoutsPath, string $prefix): MessageCatalogue { $extractedCatalogue = new MessageCatalogue($locale); $this->extractor->setPrefix($prefix); - $layoutsPath = is_array($layoutsPath) ? $layoutsPath : [$layoutsPath]; + $layoutsPath = \is_array($layoutsPath) ? $layoutsPath : [$layoutsPath]; foreach ($layoutsPath as $path) { $this->extractor->extract($path, $extractedCatalogue); } From f0e47e48c8fed3908c780265d8e77eed71f5f9b6 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Wed, 5 Feb 2025 16:23:16 +0100 Subject: [PATCH 14/16] refactor: code quality (again) --- src/Command/UtilTranslationsExtract.php | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Command/UtilTranslationsExtract.php b/src/Command/UtilTranslationsExtract.php index f678a7c26..0dfe73a03 100644 --- a/src/Command/UtilTranslationsExtract.php +++ b/src/Command/UtilTranslationsExtract.php @@ -14,8 +14,6 @@ namespace Cecil\Command; use Cecil\Exception\RuntimeException; -use Cecil\Renderer\Extension\Core as CoreExtension; -use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Bridge\Twig\Translation\TwigExtractor; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -32,8 +30,6 @@ use Symfony\Component\Translation\Writer\TranslationWriter; use Symfony\Component\Translation\Loader\PoFileLoader; use Symfony\Component\Translation\Loader\YamlFileLoader; -use Twig\Environment; -use Twig\Loader\FilesystemLoader; class UtilTranslationsExtract extends AbstractCommand { @@ -79,11 +75,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $layoutsPath = $config->getLayoutsPath(); $translationsPath = $config->getTranslationsPath(); - $format = $input->getOption('format'); - $this->initializeTranslationComponents(); - $this->checkOptions($input, $format); + $this->checkOptions($input); if ($input->getOption('theme')) { $layoutsPath = [$layoutsPath, $config->getThemeDirPath($input->getOption('theme'))]; @@ -100,7 +94,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $currentCatalogue = $this->loadCurrentMessages($input->getOption('locale'), $translationsPath); try { - $operation = $input->getOption('theme') ? new MergeOperation($currentCatalogue, $extractedCatalogue) : new TargetOperation($currentCatalogue, $extractedCatalogue); + $operation = $input->getOption('theme') + ? new MergeOperation($currentCatalogue, $extractedCatalogue) + : new TargetOperation($currentCatalogue, $extractedCatalogue); } catch (\Exception $e) { throw new RuntimeException($e->getMessage()); } @@ -119,7 +115,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int try { $this->saveDump( $operation->getResult(), - $format, + $input->getOption('format'), $translationsPath, $config->getLanguageDefault() ); @@ -131,23 +127,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - private function checkOptions(InputInterface $input, $format): void + private function checkOptions(InputInterface $input): void { if (true !== $input->getOption('save') && true !== $input->getOption('show')) { throw new RuntimeException('You must choose to display (`--show`) and/or save (`--save`) the translations'); } - if (!\in_array($format, $this->writer->getFormats(), true)) { + if (!\in_array($input->getOption('format'), $this->writer->getFormats(), true)) { throw new RuntimeException(\sprintf('Supported formats are: %s', implode(', ', $this->writer->getFormats()))); } } private function initializeTranslationComponents(): void { - // readers $this->reader = new TranslationReader(); $this->reader->addLoader('po', new PoFileLoader()); $this->reader->addLoader('yaml', new YamlFileLoader()); - // writers $this->writer = new TranslationWriter(); $this->writer->addDumper('po', new PoFileDumper()); $this->writer->addDumper('yaml', new YamlFileDumper()); From 596f6b6d6dfa4afd9a8b0d37c18902b9700ae6b5 Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Wed, 5 Feb 2025 16:30:59 +0100 Subject: [PATCH 15/16] refactor: remove useless dump option --- src/Command/UtilTranslationsExtract.php | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Command/UtilTranslationsExtract.php b/src/Command/UtilTranslationsExtract.php index 0dfe73a03..d38cdc663 100644 --- a/src/Command/UtilTranslationsExtract.php +++ b/src/Command/UtilTranslationsExtract.php @@ -113,12 +113,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int // save the file if (true === $input->getOption('save')) { try { - $this->saveDump( - $operation->getResult(), - $input->getOption('format'), - $translationsPath, - $config->getLanguageDefault() - ); + $this->saveDump($operation->getResult(), $input->getOption('format'), $translationsPath); } catch (\InvalidArgumentException $e) { throw new RuntimeException('Error while saving translation file: ' . $e->getMessage()); } @@ -175,13 +170,10 @@ private function loadCurrentMessages(string $locale, string $translationsPath): return $currentCatalogue; } - private function saveDump(MessageCatalogueInterface $messageCatalogue, string $format, string $translationsPath, string $defaultLocale): void + private function saveDump(MessageCatalogueInterface $messageCatalogue, string $format, string $translationsPath): void { $this->io->writeln('Writing file...'); - $this->writer->write($messageCatalogue, $format, [ - 'path' => $translationsPath, - 'default_locale' => $defaultLocale, - ]); + $this->writer->write($messageCatalogue, $format, ['path' => $translationsPath]); $this->io->success('Translation file have been successfully updated.'); } From 8deb12262306a6fd859271c6615626666cf0739b Mon Sep 17 00:00:00 2001 From: Arnaud Ligny Date: Wed, 5 Feb 2025 19:10:21 +0100 Subject: [PATCH 16/16] refactor: last one? --- src/Command/UtilTranslationsExtract.php | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Command/UtilTranslationsExtract.php b/src/Command/UtilTranslationsExtract.php index d38cdc663..59c57cf09 100644 --- a/src/Command/UtilTranslationsExtract.php +++ b/src/Command/UtilTranslationsExtract.php @@ -75,7 +75,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $layoutsPath = $config->getLayoutsPath(); $translationsPath = $config->getTranslationsPath(); - $this->initializeTranslationComponents(); + $this->initTranslationComponents(); $this->checkOptions($input); @@ -83,7 +83,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $layoutsPath = [$layoutsPath, $config->getThemeDirPath($input->getOption('theme'))]; } - $this->initializeTwigExtractor($layoutsPath); + $this->initTwigExtractor($layoutsPath); $output->writeln(\sprintf('Generating "%s" translation file', $input->getOption('locale'))); @@ -93,6 +93,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln('Loading translation file...'); $currentCatalogue = $this->loadCurrentMessages($input->getOption('locale'), $translationsPath); + // processing translations catalogues try { $operation = $input->getOption('theme') ? new MergeOperation($currentCatalogue, $extractedCatalogue) @@ -132,7 +133,7 @@ private function checkOptions(InputInterface $input): void } } - private function initializeTranslationComponents(): void + private function initTranslationComponents(): void { $this->reader = new TranslationReader(); $this->reader->addLoader('po', new PoFileLoader()); @@ -142,7 +143,7 @@ private function initializeTranslationComponents(): void $this->writer->addDumper('yaml', new YamlFileDumper()); } - private function initializeTwigExtractor($layoutsPath = []): void + private function initTwigExtractor($layoutsPath = []): void { $twig = (new \Cecil\Renderer\Twig($this->getBuilder(), $layoutsPath))->getTwig(); $this->extractor = new TwigExtractor($twig); @@ -199,11 +200,7 @@ private function dumpMessages(OperationInterface $operation): void } $this->io->success( - \sprintf( - '%d message%s successfully extracted.', - $messagesCount, - $messagesCount > 1 ? 's were' : ' was' - ) + \sprintf('%d message%s successfully extracted.', $messagesCount, $messagesCount > 1 ? 's were' : ' was') ); } }