Improve script to find translation duplicates

Format the output using a table.
Use existing translator.default service to fetch translation files and extract keys and translations from the translation catalogue for a certain locale.
This commit is contained in:
Julie Lenaerts 2024-09-17 12:32:51 +02:00
parent 155066be13
commit 23d882d4cd

View File

@ -15,118 +15,115 @@ use Symfony\Component\Console\Command\Command;
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;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
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';
private const string BASE_DIR = 'vendor/chill-project/chill-bundles'; public function __construct(private readonly TranslatorInterface $translator, private readonly KernelInterface $kernel)
{
parent::__construct();
}
protected function configure(): void protected function configure(): void
{ {
$this $this
->setDescription('Detects duplicate translations in YAML files.') ->setDescription('Detects duplicate translations in YAML files.')
->addOption('path', null, InputOption::VALUE_REQUIRED, 'The directory or file path containing translation files', 'translations'); ->addOption('locale', null, InputOption::VALUE_REQUIRED, 'Locale to check for duplicate translations', 'en');
// ->addOption('output', null, InputOption::VALUE_OPTIONAL, 'The file path to write the output to', 'php://stdout');
} }
protected function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output): int
{ {
$relativePath = $input->getOption('path'); $locale = $input->getOption('locale');
$basePath = self::BASE_DIR;
$fullPath = $basePath . '/' . $relativePath;
// $outputPath = $input->getOption('output'); // Loop through all bundles and get the translation directories
foreach ($this->kernel->getBundles() as $bundle) {
$bundlePath = $bundle->getPath();
$translationDir = $this->getTranslationDirectory($bundle->getName(), $bundlePath);
if (!file_exists($fullPath)) { if ($translationDir && is_dir($translationDir)) {
$output->writeln("<error>The path '$fullPath' does not exist.</error>"); foreach (glob($translationDir . '/*.yaml') as $file) {
return Command::FAILURE; $this->translator->addResource('yaml', $file, $locale);
} }
$translationMap = [];
if (is_dir($fullPath)) {
// Process all YAML files in the directory
foreach (glob($fullPath . '/*.yaml') as $file) {
$this->processFile($file, $translationMap);
} }
} elseif (is_file($fullPath)) {
// Process the specific YAML file
$this->processFile($fullPath, $translationMap);
} else {
$output->writeln("<error>The path '$fullPath' is neither a directory nor a file.</error>");
return Command::FAILURE;
} }
$duplicates = array_filter($translationMap, function($keys) { $catalogue = $this->translator->getCatalogue($locale);
$allTranslations = [];
// Iterate through each domain in the catalogue
foreach ($catalogue->all() as $domain => $translations) {
foreach ($translations as $key => $value) {
if (is_array($value)) {
$this->flattenTranslation($value, "$domain.$key", $allTranslations);
} else {
if (!isset($allTranslations[$value])) {
$allTranslations[$value] = [];
}
$allTranslations[$value][] = "$domain.$key";
}
}
}
// Detect values that appear in more than one key
$duplicates = array_filter($allTranslations, function ($keys) {
return count($keys) > 1; return count($keys) > 1;
}); });
if (empty($duplicates)) { if (empty($duplicates)) {
$output->writeln("<info>No duplicate translations found.</info>"); $output->writeln("<info>No duplicate translations found for locale '$locale'.</info>");
} else { } else {
$output->writeln("<comment>Duplicate translations found:</comment>"); $output->writeln("<comment>Duplicate translations found for locale '$locale':</comment>");
foreach ($duplicates as $translation => $keys) {
$output->writeln("<info>Translation:</info> '$translation' <info>is used in keys:</info> " . implode(', ', $keys));
}
}
/* $outputText = ""; // Display the duplicates in a table
if (empty($duplicates)) { $table = new Table($output);
$outputText .= "No duplicate translations found.\n"; $table->setHeaders(['Translation', 'Used in Keys']);
} else {
$outputText .= "Duplicate translations found:\n";
foreach ($duplicates as $translation => $keys) {
$outputText .= "Translation: '$translation' is used in keys: " . implode(', ', $keys) . "\n";
}
}
// Write output to the specified file or to stdout foreach ($duplicates as $translation => $keys) {
if ($outputPath === 'php://stdout') { $wrappedTranslation = $this->wrapText($translation, 40);
$output->writeln($outputText); $wrappedKeys = $this->wrapText(implode(', ', $keys), 80);
} else {
try { $table->addRow([$wrappedTranslation, $wrappedKeys]);
file_put_contents($outputPath, $outputText);
$output->writeln("<info>Output written to '$outputPath'</info>");
} catch (\Exception $e) {
$output->writeln("<error>Failed to write to '$outputPath': " . $e->getMessage() . "</error>");
return Command::FAILURE;
} }
}*/
$table->render();
}
return Command::SUCCESS; return Command::SUCCESS;
} }
private function processFile($file, &$translationMap): void private function flattenTranslation(array $translations, string $prefix, array &$allTranslations): void
{ {
try { foreach ($translations as $key => $value) {
$translations = Yaml::parseFile($file); $fullKey = "$prefix.$key";
// Flatten the array to handle nested keys
$this->flattenArray($translations, '', $translationMap);
} catch (\Exception $e) {
// Handle YAML parsing exceptions
// You might want to log the error or notify the user
}
}
private function flattenArray(array $array, $prefix, &$translationMap): void
{
foreach ($array as $key => $value) {
$fullKey = $prefix ? $prefix . '.' . $key : $key;
if (is_array($value)) { if (is_array($value)) {
// Recursively process nested arrays $this->flattenTranslation($value, $fullKey, $allTranslations);
$this->flattenArray($value, $fullKey, $translationMap);
} else { } else {
// Handle translation value if (!isset($allTranslations[$value])) {
if (isset($translationMap[$value])) { $allTranslations[$value] = [];
$translationMap[$value][] = $fullKey;
} else {
$translationMap[$value] = [$fullKey];
} }
$allTranslations[$value][] = $fullKey;
} }
} }
} }
private function getTranslationDirectory(string $bundleName, string $bundlePath): ?string
{
$translationDir = $bundlePath . '/translations';
if ($bundleName === 'ChillAsideActivityBundle') {
$translationDir = $bundlePath . '/src/translations';
}
return is_dir($translationDir) ? $translationDir : null;
}
private function wrapText(string $text, int $width): string
{
return wordwrap($text, $width, "\n", true);
}
} }