diff --git a/src/Bundle/ChillMainBundle/Command/DetectMissingTranslationsCommand.php b/src/Bundle/ChillMainBundle/Command/DetectMissingTranslationsCommand.php new file mode 100644 index 000000000..12a194699 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Command/DetectMissingTranslationsCommand.php @@ -0,0 +1,199 @@ +setDescription('Checks for missing translations in the specified locale.') + ->addOption('bundle', null, InputOption::VALUE_REQUIRED, 'The relative path to the translation files', 'translations') + ->addOption('locale', null, InputOption::VALUE_REQUIRED, 'The locale to check for missing translations', 'fr'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $locale = $input->getOption('locale'); + $bundle = $input->getOption('bundle'); + $bundlePath = self::BASE_DIR . '/' . $bundle; +// $vuePath = $bundlePath . '/Resources/public/vuejs'; + $twigPath = $bundlePath . '/Resources/public'; + $localeFile = $bundlePath . "/translations/messages.$locale.yml"; + + if (!file_exists($localeFile)) { + $output->writeln("The locale file '$localeFile' does not exist."); + return Command::FAILURE; + } + + $missingKeys = []; + + // Load existing translations for the specified locale + $existingTranslations = Yaml::parseFile($localeFile); + + // Extract translation keys from Vue components +/* $vueKeys = $this->extractVueKeys($vuePath); + foreach ($vueKeys as $key) { + if (!$this->keyExistsInTranslations($key, $existingTranslations)) { + $missingKeys[$key] = 'Missing translation in Vue component'; + } + }*/ + + // Extract translation keys from Twig templates + $twigKeys = $this->extractTwigKeys($twigPath); + foreach ($twigKeys as $key) { + if (!$this->keyExistsInTranslations($key, $existingTranslations)) { + $missingKeys[$key] = 'Missing translation in Twig template'; + } + } + + // Extract translation keys from PHP code + $phpKeys = $this->extractPhpKeys($bundlePath); + foreach ($phpKeys as $key) { + if (!$this->keyExistsInTranslations($key, $existingTranslations)) { + $missingKeys[$key] = 'Missing translation in PHP code'; + } + } + + if (empty($missingKeys)) { + $output->writeln("No missing translations found."); + } else { + $output->writeln("Missing translations:"); + foreach ($missingKeys as $key => $info) { + $output->writeln("$key: $info"); + } + + // Prompt user to add missing translations + $this->promptForTranslations($missingKeys, $localeFile); + } + + return Command::SUCCESS; + } + + private function extractVueKeys($path) + { + $keys = []; + $files = $this->getAllFiles($path, ['vue']); + + foreach ($files as $file) { + $content = file_get_contents($file); + preg_match_all('/\$t\((\'|")(.*?)(\'|")\)/', $content, $matches); + foreach ($matches[2] as $match) { + $keys[] = $match; + } + } + + return array_unique($keys); + } + + private function extractTwigKeys($path) + { + $keys = []; + $files = $this->getAllFiles($path, ['html.twig', 'twig']); + + foreach ($files as $file) { + $content = file_get_contents($file); + preg_match_all('/\|trans\s*\(\'(.*?)\'/', $content, $matches); + foreach ($matches[1] as $match) { + $keys[] = $match; + } + } + + return array_unique($keys); + } + + private function extractPhpKeys($path) + { + $keys = []; + $files = $this->getAllFiles($path, ['php']); + + foreach ($files as $file) { + $content = file_get_contents($file); + preg_match_all('/->trans\((\'|")(.*?)(\'|")\)/', $content, $matches); + foreach ($matches[2] as $match) { + $keys[] = $match; + } + } + + return array_unique($keys); + } + + private function getAllFiles($directory, array $extensions) + { + $files = []; + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory)); + + foreach ($iterator as $file) { + if ($file->isFile() && in_array($file->getExtension(), $extensions)) { + $files[] = $file->getPathname(); + } + } + + return $files; + } + + private function keyExistsInTranslations($key, $translations) + { + $keys = explode('.', $key); + $current = $translations; + + foreach ($keys as $part) { + if (isset($current[$part])) { + $current = $current[$part]; + } else { + return false; + } + } + + return true; + } + + private function promptForTranslations($missingKeys, $localeFile) + { + $fs = new Filesystem(); + foreach ($missingKeys as $key => $info) { + $translation = readline("Enter translation for '$key': "); + if ($translation !== '') { + // Add translation to the YAML file + $existing = file_exists($localeFile) ? Yaml::parseFile($localeFile) : []; + $this->addTranslation($existing, $key, $translation); + $fs->dumpFile($localeFile, Yaml::dump($existing)); + } + } + } + + private function addTranslation(&$translations, $key, $translation) + { + $keys = explode('.', $key); + $current = &$translations; + + foreach ($keys as $part) { + if (!isset($current[$part])) { + $current[$part] = []; + } + $current = &$current[$part]; + } + + $current = $translation; + } +} diff --git a/src/Bundle/ChillMainBundle/Command/DetectTranslationDuplicatesCommand.php b/src/Bundle/ChillMainBundle/Command/DetectTranslationDuplicatesCommand.php new file mode 100644 index 000000000..56ccebba2 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Command/DetectTranslationDuplicatesCommand.php @@ -0,0 +1,132 @@ +setDescription('Detects duplicate translations in YAML files.') + ->addOption('path', null, InputOption::VALUE_REQUIRED, 'The directory or file path containing translation files', 'translations'); +// ->addOption('output', null, InputOption::VALUE_OPTIONAL, 'The file path to write the output to', 'php://stdout'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $relativePath = $input->getOption('path'); + $basePath = self::BASE_DIR; + $fullPath = $basePath . '/' . $relativePath; + +// $outputPath = $input->getOption('output'); + + if (!file_exists($fullPath)) { + $output->writeln("The path '$fullPath' does not exist."); + return Command::FAILURE; + } + + $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("The path '$fullPath' is neither a directory nor a file."); + return Command::FAILURE; + } + + $duplicates = array_filter($translationMap, function($keys) { + return count($keys) > 1; + }); + + if (empty($duplicates)) { + $output->writeln("No duplicate translations found."); + } else { + $output->writeln("Duplicate translations found:"); + foreach ($duplicates as $translation => $keys) { + $output->writeln("Translation: '$translation' is used in keys: " . implode(', ', $keys)); + } + } + +/* $outputText = ""; + if (empty($duplicates)) { + $outputText .= "No duplicate translations found.\n"; + } 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 + if ($outputPath === 'php://stdout') { + $output->writeln($outputText); + } else { + try { + file_put_contents($outputPath, $outputText); + $output->writeln("Output written to '$outputPath'"); + } catch (\Exception $e) { + $output->writeln("Failed to write to '$outputPath': " . $e->getMessage() . ""); + return Command::FAILURE; + } + }*/ + + return Command::SUCCESS; + } + + private function processFile($file, &$translationMap): void + { + try { + $translations = Yaml::parseFile($file); + + // 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)) { + // Recursively process nested arrays + $this->flattenArray($value, $fullKey, $translationMap); + } else { + // Handle translation value + if (isset($translationMap[$value])) { + $translationMap[$value][] = $fullKey; + } else { + $translationMap[$value] = [$fullKey]; + } + } + } + } +} diff --git a/src/Bundle/ChillMainBundle/config/services/command.yaml b/src/Bundle/ChillMainBundle/config/services/command.yaml index 94cb2cf97..106b01f37 100644 --- a/src/Bundle/ChillMainBundle/config/services/command.yaml +++ b/src/Bundle/ChillMainBundle/config/services/command.yaml @@ -74,3 +74,11 @@ services: Chill\MainBundle\Command\SynchronizeEntityInfoViewsCommand: tags: - {name: console.command} + + Chill\MainBundle\Command\DetectTranslationDuplicatesCommand: + tags: + - { name: console.command } + + Chill\MainBundle\Command\DetectMissingTranslationsCommand: + tags: + - { name: console.command }