blob: dba21643da6692ccc374442c62d71f9b6c10a393 [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Translation\Command;
13
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010014use Symfony\Component\Console\Attribute\AsCommand;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020015use Symfony\Component\Console\Command\Command;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010016use Symfony\Component\Console\Completion\CompletionInput;
17use Symfony\Component\Console\Completion\CompletionSuggestions;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020018use Symfony\Component\Console\Exception\InvalidArgumentException;
19use Symfony\Component\Console\Input\InputArgument;
20use Symfony\Component\Console\Input\InputInterface;
21use Symfony\Component\Console\Input\InputOption;
22use Symfony\Component\Console\Output\OutputInterface;
23use Symfony\Component\Console\Style\SymfonyStyle;
24use Symfony\Component\Translation\Provider\TranslationProviderCollection;
25use Symfony\Component\Translation\Reader\TranslationReaderInterface;
26use Symfony\Component\Translation\TranslatorBag;
27
28/**
29 * @author Mathieu Santostefano <msantostefano@protonmail.com>
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020030 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010031#[AsCommand(name: 'translation:push', description: 'Push translations to a given provider.')]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020032final class TranslationPushCommand extends Command
33{
34 use TranslationTrait;
35
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020036 private $providers;
37 private $reader;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010038 private array $transPaths;
39 private array $enabledLocales;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020040
41 public function __construct(TranslationProviderCollection $providers, TranslationReaderInterface $reader, array $transPaths = [], array $enabledLocales = [])
42 {
43 $this->providers = $providers;
44 $this->reader = $reader;
45 $this->transPaths = $transPaths;
46 $this->enabledLocales = $enabledLocales;
47
48 parent::__construct();
49 }
50
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010051 public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
52 {
53 if ($input->mustSuggestArgumentValuesFor('provider')) {
54 $suggestions->suggestValues($this->providers->keys());
55
56 return;
57 }
58
59 if ($input->mustSuggestOptionValuesFor('domains')) {
60 $provider = $this->providers->get($input->getArgument('provider'));
61
62 if ($provider && method_exists($provider, 'getDomains')) {
63 $domains = $provider->getDomains();
64 $suggestions->suggestValues($domains);
65 }
66
67 return;
68 }
69
70 if ($input->mustSuggestOptionValuesFor('locales')) {
71 $suggestions->suggestValues($this->enabledLocales);
72 }
73 }
74
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020075 /**
76 * {@inheritdoc}
77 */
78 protected function configure()
79 {
80 $keys = $this->providers->keys();
81 $defaultProvider = 1 === \count($keys) ? $keys[0] : null;
82
83 $this
84 ->setDefinition([
85 new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to push translations to.', $defaultProvider),
86 new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with local ones (it will delete not synchronized messages).'),
87 new InputOption('delete-missing', null, InputOption::VALUE_NONE, 'Delete translations available on provider but not locally.'),
88 new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'),
89 new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales),
90 ])
91 ->setHelp(<<<'EOF'
92The <info>%command.name%</> command pushes translations to the given provider. Only new
93translations are pushed, existing ones are not overwritten.
94
95You can overwrite existing translations by using the <comment>--force</> flag:
96
97 <info>php %command.full_name% --force provider</>
98
99You can delete provider translations which are not present locally by using the <comment>--delete-missing</> flag:
100
101 <info>php %command.full_name% --delete-missing provider</>
102
103Full example:
104
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100105 <info>php %command.full_name% provider --force --delete-missing --domains=messages --domains=validators --locales=en</>
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200106
107This command pushes all translations associated with the <comment>messages</> and <comment>validators</> domains for the <comment>en</> locale.
108Provider translations for the specified domains and locale are deleted if they're not present locally and overwritten if it's the case.
109Provider translations for others domains and locales are ignored.
110EOF
111 )
112 ;
113 }
114
115 /**
116 * {@inheritdoc}
117 */
118 protected function execute(InputInterface $input, OutputInterface $output): int
119 {
120 $provider = $this->providers->get($input->getArgument('provider'));
121
122 if (!$this->enabledLocales) {
123 throw new InvalidArgumentException(sprintf('You must define "framework.translator.enabled_locales" or "framework.translator.providers.%s.locales" config key in order to work with translation providers.', parse_url($provider, \PHP_URL_SCHEME)));
124 }
125
126 $io = new SymfonyStyle($input, $output);
127 $domains = $input->getOption('domains');
128 $locales = $input->getOption('locales');
129 $force = $input->getOption('force');
130 $deleteMissing = $input->getOption('delete-missing');
131
132 $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths);
133
134 if (!$domains) {
135 $domains = $this->getDomainsFromTranslatorBag($localTranslations);
136 }
137
138 if (!$deleteMissing && $force) {
139 $provider->write($localTranslations);
140
141 $io->success(sprintf('All local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
142
143 return 0;
144 }
145
146 $providerTranslations = $provider->read($domains, $locales);
147
148 if ($deleteMissing) {
149 $provider->delete($providerTranslations->diff($localTranslations));
150
151 $io->success(sprintf('Missing translations on "%s" has been deleted (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
152
153 // Read provider translations again, after missing translations deletion,
154 // to avoid push freshly deleted translations.
155 $providerTranslations = $provider->read($domains, $locales);
156 }
157
158 $translationsToWrite = $localTranslations->diff($providerTranslations);
159
160 if ($force) {
161 $translationsToWrite->addBag($localTranslations->intersect($providerTranslations));
162 }
163
164 $provider->write($translationsToWrite);
165
166 $io->success(sprintf('%s local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', $force ? 'All' : 'New', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
167
168 return 0;
169 }
170
171 private function getDomainsFromTranslatorBag(TranslatorBag $translatorBag): array
172 {
173 $domains = [];
174
175 foreach ($translatorBag->getCatalogues() as $catalogue) {
176 $domains += $catalogue->getDomains();
177 }
178
179 return array_unique($domains);
180 }
181}