Skip to content

Commit

Permalink
feat: translations extraction command (#2045)
Browse files Browse the repository at this point in the history
Introduced a new `util:translations:extract` command to facilitate extraction and processing of translation strings from layouts.

---------

Signed-off-by: Franck Matsos <[email protected]>
Co-authored-by: Arnaud Ligny <[email protected]>
  • Loading branch information
fmatsos and ArnaudLigny authored Feb 6, 2025
1 parent 6672c8e commit b060c8f
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ protected function getDefaultCommands(): array
new Command\ShowContent(),
new Command\ShowConfig(),
new Command\ListCommand(),
new Command\UtilTranslationsExtract()
];
if (Util\Platform::isPhar()) {
$commands[] = new Command\SelfUpdate();
Expand Down
206 changes: 206 additions & 0 deletions src/Command/UtilTranslationsExtract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<?php

declare(strict_types=1);

/*
* This file is part of Cecil.
*
* Copyright (c) Arnaud Ligny <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Cecil\Command;

use Cecil\Exception\RuntimeException;
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\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;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
use Symfony\Component\Translation\Reader\TranslationReader;
use Symfony\Component\Translation\Writer\TranslationWriter;
use Symfony\Component\Translation\Loader\PoFileLoader;
use Symfony\Component\Translation\Loader\YamlFileLoader;

class UtilTranslationsExtract extends AbstractCommand
{
private TranslationWriter $writer;
private TranslationReader $reader;
private TwigExtractor $extractor;

protected function configure(): void
{
$this
->setName('util:translations:extract')
->setDescription('Extracts translations from layouts')
->setDefinition([
new InputArgument('path', InputArgument::OPTIONAL, 'Use the given path as working directory'),
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('theme', null, InputOption::VALUE_OPTIONAL, 'Use if you want to translate a theme layouts too'),
])
->setHelp(
<<<'EOF'
The <info>%command.name%</info> 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 <info>NEW_</info> prefix to the translation message.
Example running against working directory:
<info>php %command.full_name% --show</info>
<info>php %command.full_name% --save --locale=en</info>
You can extract, and merge, translations from a given theme with <comment>--theme</> option:
<info>php %command.full_name% --show --theme=hyde</info>
EOF
)
;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$config = $this->getBuilder()->getConfig();
$layoutsPath = $config->getLayoutsPath();
$translationsPath = $config->getTranslationsPath();

$this->initTranslationComponents();

$this->checkOptions($input);

if ($input->getOption('theme')) {
$layoutsPath = [$layoutsPath, $config->getThemeDirPath($input->getOption('theme'))];
}

$this->initTwigExtractor($layoutsPath);

$output->writeln(\sprintf('Generating "<info>%s</info>" translation file', $input->getOption('locale')));

$output->writeln('Parsing templates...');
$extractedCatalogue = $this->extractMessages($input->getOption('locale'), $layoutsPath, 'NEW_');

$output->writeln('Loading translation file...');
$currentCatalogue = $this->loadCurrentMessages($input->getOption('locale'), $translationsPath);

// processing translations catalogues
try {
$operation = $input->getOption('theme')
? new MergeOperation($currentCatalogue, $extractedCatalogue)
: new TargetOperation($currentCatalogue, $extractedCatalogue);
} catch (\Exception $e) {
throw new RuntimeException($e->getMessage());
}

// show compiled list of messages
if (true === $input->getOption('show')) {
try {
$this->dumpMessages($operation);
} catch (\Exception $e) {
throw new RuntimeException('Error while displaying messages: ' . $e->getMessage());
}
}

// save the file
if (true === $input->getOption('save')) {
try {
$this->saveDump($operation->getResult(), $input->getOption('format'), $translationsPath);
} catch (\InvalidArgumentException $e) {
throw new RuntimeException('Error while saving translation file: ' . $e->getMessage());
}
}

return 0;
}

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($input->getOption('format'), $this->writer->getFormats(), true)) {
throw new RuntimeException(\sprintf('Supported formats are: %s', implode(', ', $this->writer->getFormats())));
}
}

private function initTranslationComponents(): void
{
$this->reader = new TranslationReader();
$this->reader->addLoader('po', new PoFileLoader());
$this->reader->addLoader('yaml', new YamlFileLoader());
$this->writer = new TranslationWriter();
$this->writer->addDumper('po', new PoFileDumper());
$this->writer->addDumper('yaml', new YamlFileDumper());
}

private function initTwigExtractor($layoutsPath = []): void
{
$twig = (new \Cecil\Renderer\Twig($this->getBuilder(), $layoutsPath))->getTwig();
$this->extractor = new TwigExtractor($twig);
}

private function extractMessages(string $locale, $layoutsPath, string $prefix): MessageCatalogue
{
$extractedCatalogue = new MessageCatalogue($locale);
$this->extractor->setPrefix($prefix);
$layoutsPath = \is_array($layoutsPath) ? $layoutsPath : [$layoutsPath];
foreach ($layoutsPath as $path) {
$this->extractor->extract($path, $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 saveDump(MessageCatalogueInterface $messageCatalogue, string $format, string $translationsPath): void
{
$this->io->writeln('Writing file...');
$this->writer->write($messageCatalogue, $format, ['path' => $translationsPath]);
$this->io->success('Translation file have been successfully updated.');
}

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('<fg=green>%s</>', $key), $newKeys),
array_map(
fn ($key) => \sprintf('<fg=red>%s</>', $key),
array_keys($operation->getObsoleteMessages($domain))
)
);
$domainMessagesCount = \count($list);
sort($list);
$this->io->listing($list);
$messagesCount += $domainMessagesCount;
}

$this->io->success(
\sprintf('%d message%s successfully extracted.', $messagesCount, $messagesCount > 1 ? 's were' : ' was')
);
}
}
8 changes: 8 additions & 0 deletions src/Renderer/Twig.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,12 @@ public function getDebugProfile(): ?\Twig\Profiler\Profile
{
return $this->profile;
}

/**
* Returns the Twig instance.
*/
public function getTwig(): \Twig\Environment
{
return $this->twig;
}
}

0 comments on commit b060c8f

Please sign in to comment.