TranslationLintCommand.php 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Translation\Command;
  11. use Symfony\Component\Console\Attribute\AsCommand;
  12. use Symfony\Component\Console\Command\Command;
  13. use Symfony\Component\Console\Completion\CompletionInput;
  14. use Symfony\Component\Console\Completion\CompletionSuggestions;
  15. use Symfony\Component\Console\Input\InputInterface;
  16. use Symfony\Component\Console\Input\InputOption;
  17. use Symfony\Component\Console\Output\OutputInterface;
  18. use Symfony\Component\Console\Style\SymfonyStyle;
  19. use Symfony\Component\Translation\Exception\ExceptionInterface;
  20. use Symfony\Component\Translation\TranslatorBagInterface;
  21. use Symfony\Contracts\Translation\TranslatorInterface;
  22. /**
  23. * Lint translations files syntax and outputs encountered errors.
  24. *
  25. * @author Hugo Alliaume <hugo@alliau.me>
  26. */
  27. #[AsCommand(name: 'lint:translations', description: 'Lint translations files syntax and outputs encountered errors')]
  28. class TranslationLintCommand extends Command
  29. {
  30. private SymfonyStyle $io;
  31. public function __construct(
  32. private TranslatorInterface&TranslatorBagInterface $translator,
  33. private array $enabledLocales = [],
  34. ) {
  35. parent::__construct();
  36. }
  37. public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
  38. {
  39. if ($input->mustSuggestOptionValuesFor('locale')) {
  40. $suggestions->suggestValues($this->enabledLocales);
  41. }
  42. }
  43. protected function configure(): void
  44. {
  45. $this
  46. ->setDefinition([
  47. new InputOption('locale', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the locales to lint.', $this->enabledLocales),
  48. ])
  49. ->setHelp(<<<'EOF'
  50. The <info>%command.name%</> command lint translations.
  51. <info>php %command.full_name%</>
  52. EOF
  53. );
  54. }
  55. protected function initialize(InputInterface $input, OutputInterface $output): void
  56. {
  57. $this->io = new SymfonyStyle($input, $output);
  58. }
  59. protected function execute(InputInterface $input, OutputInterface $output): int
  60. {
  61. $locales = $input->getOption('locale');
  62. /** @var array<string, array<string, array<string, \Throwable>> $errors */
  63. $errors = [];
  64. $domainsByLocales = [];
  65. foreach ($locales as $locale) {
  66. $messageCatalogue = $this->translator->getCatalogue($locale);
  67. foreach ($domainsByLocales[$locale] = $messageCatalogue->getDomains() as $domain) {
  68. foreach ($messageCatalogue->all($domain) as $id => $translation) {
  69. try {
  70. $this->translator->trans($id, [], $domain, $messageCatalogue->getLocale());
  71. } catch (ExceptionInterface $e) {
  72. $errors[$locale][$domain][$id] = $e;
  73. }
  74. }
  75. }
  76. }
  77. if (!$domainsByLocales) {
  78. $this->io->error('No translation files were found.');
  79. return Command::SUCCESS;
  80. }
  81. $this->io->table(
  82. ['Locale', 'Domains', 'Valid?'],
  83. array_map(
  84. static fn (string $locale, array $domains) => [
  85. $locale,
  86. implode(', ', $domains),
  87. !\array_key_exists($locale, $errors) ? '<info>Yes</>' : '<error>No</>',
  88. ],
  89. array_keys($domainsByLocales),
  90. $domainsByLocales
  91. ),
  92. );
  93. if ($errors) {
  94. foreach ($errors as $locale => $domains) {
  95. foreach ($domains as $domain => $domainsErrors) {
  96. $this->io->section(\sprintf('Errors for locale "%s" and domain "%s"', $locale, $domain));
  97. foreach ($domainsErrors as $id => $error) {
  98. $this->io->text(\sprintf('Translation key "%s" is invalid:', $id));
  99. $this->io->error($error->getMessage());
  100. }
  101. }
  102. }
  103. return Command::FAILURE;
  104. }
  105. $this->io->success('All translations are valid.');
  106. return Command::SUCCESS;
  107. }
  108. }