Expand detection of duplicates in translations command

Added functionality to specify key namespaces that should be exluded from duplicate detection
Added functionality to check the accidental creation of duplicates based on an md5 hash of translation duplicates.
This commit is contained in:
Julie Lenaerts 2024-09-20 12:16:31 +02:00
parent 611261c863
commit 41f13e29e0

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Command; namespace Chill\MainBundle\Command;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -19,6 +20,7 @@ use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\Table;
class DetectTranslationDuplicatesCommand extends Command class DetectTranslationDuplicatesCommand extends Command
{ {
protected static $defaultName = 'app:detect-duplicate-translations'; protected static $defaultName = 'app:detect-duplicate-translations';
@ -32,12 +34,16 @@ class DetectTranslationDuplicatesCommand extends Command
{ {
$this $this
->setDescription('Detects duplicate translations in YAML files.') ->setDescription('Detects duplicate translations in YAML files.')
->addOption('locale', null, InputOption::VALUE_REQUIRED, 'Locale to check for duplicate translations', 'en'); ->addOption('locale', null, InputOption::VALUE_REQUIRED, 'Locale to check for duplicate translations', 'en')
->addOption('exclude-namespaces', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Namespaces to exclude from duplicate detection', [])
->addArgument('verify-hash', InputArgument::OPTIONAL, 'The expected hash to verify translation integrity');
} }
protected function execute(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output): int
{ {
$locale = $input->getOption('locale'); $locale = $input->getOption('locale');
$excludedNamespaces = $input->getOption('exclude-namespaces');
$expectedHash = $input->getArgument('verify-hash');
// Loop through all bundles and get the translation directories // Loop through all bundles and get the translation directories
foreach ($this->kernel->getBundles() as $bundle) { foreach ($this->kernel->getBundles() as $bundle) {
@ -58,6 +64,9 @@ class DetectTranslationDuplicatesCommand extends Command
// Iterate through each domain in the catalogue // Iterate through each domain in the catalogue
foreach ($catalogue->all() as $domain => $translations) { foreach ($catalogue->all() as $domain => $translations) {
foreach ($translations as $key => $value) { foreach ($translations as $key => $value) {
if ($this->isExcludedNamespace("$domain.$key", $excludedNamespaces)) {
continue;
}
if (is_array($value)) { if (is_array($value)) {
$this->flattenTranslation($value, "$domain.$key", $allTranslations); $this->flattenTranslation($value, "$domain.$key", $allTranslations);
} else { } else {
@ -74,25 +83,27 @@ class DetectTranslationDuplicatesCommand extends Command
return count($keys) > 1; return count($keys) > 1;
}); });
if (empty($duplicates)) { $duplicatesHash = $this->generateDuplicatesHash($duplicates);
$output->writeln("<info>No duplicate translations found for locale '$locale'.</info>");
} else {
$output->writeln("<comment>Duplicate translations found for locale '$locale':</comment>");
// Display the duplicates in a table if ($expectedHash) {
$table = new Table($output); if ($duplicatesHash === $expectedHash) {
$table->setHeaders(['Translation', 'Used in Keys']); $output->writeln('<info>Translations are consistent with the expected hash.</info>');
foreach ($duplicates as $translation => $keys) { $output->writeln("<info>Current duplicate hash: $duplicatesHash</info>");
$wrappedTranslation = $this->wrapText($translation, 40); return Command::SUCCESS;
$wrappedKeys = $this->wrapText(implode(', ', $keys), 80); } else {
$output->writeln('<error>Translation hash mismatch! Potential duplicate added.</error>');
$this->renderDuplicatesTable($output, $duplicates, $locale);
$table->addRow([$wrappedTranslation, $wrappedKeys]); $output->writeln("<info>Current duplicate hash: $duplicatesHash</info>");
return Command::FAILURE;
} }
$table->render();
} }
$this->renderDuplicatesTable($output, $duplicates, $locale);
$output->writeln("<info>Current duplicate hash: $duplicatesHash</info>");
return Command::SUCCESS; return Command::SUCCESS;
} }
@ -126,4 +137,44 @@ class DetectTranslationDuplicatesCommand extends Command
{ {
return wordwrap($text, $width, "\n", true); return wordwrap($text, $width, "\n", true);
} }
private function isExcludedNamespace(string $key, array $excludedNamespaces): bool
{
foreach ($excludedNamespaces as $namespace) {
if (str_starts_with($key, $namespace)) {
return true;
}
}
return false;
}
private function generateDuplicatesHash(array $duplicates): string
{
ksort($duplicates);
foreach ($duplicates as $translation => $keys) {
sort($keys);
}
return hash('md5', serialize($duplicates));
}
private function renderDuplicatesTable(OutputInterface $output, array $duplicates, string $locale): void
{
if (empty($duplicates)) {
$output->writeln("<info>No duplicate translations found for locale '$locale'.</info>");
return;
}
$output->writeln("<comment>Duplicate translations found for locale '$locale':</comment>");
$table = new Table($output);
$table->setHeaders(['Translation', 'Used in Keys']);
foreach ($duplicates as $translation => $keys) {
$wrappedTranslation = $this->wrapText($translation, 40);
$wrappedKeys = $this->wrapText(implode(', ', $keys), 80);
$table->addRow([$wrappedTranslation, $wrappedKeys]);
}
$table->render();
}
} }