v.0.1
This commit is contained in:
		
							
								
								
									
										184
									
								
								vendor/symfony/translation/Command/TranslationPullCommand.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								vendor/symfony/translation/Command/TranslationPullCommand.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,184 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of the Symfony package.
 | 
			
		||||
 *
 | 
			
		||||
 * (c) Fabien Potencier <fabien@symfony.com>
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view the LICENSE
 | 
			
		||||
 * file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Symfony\Component\Translation\Command;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Console\Attribute\AsCommand;
 | 
			
		||||
use Symfony\Component\Console\Command\Command;
 | 
			
		||||
use Symfony\Component\Console\Completion\CompletionInput;
 | 
			
		||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
 | 
			
		||||
use Symfony\Component\Console\Input\InputArgument;
 | 
			
		||||
use Symfony\Component\Console\Input\InputInterface;
 | 
			
		||||
use Symfony\Component\Console\Input\InputOption;
 | 
			
		||||
use Symfony\Component\Console\Output\OutputInterface;
 | 
			
		||||
use Symfony\Component\Console\Style\SymfonyStyle;
 | 
			
		||||
use Symfony\Component\Translation\Catalogue\TargetOperation;
 | 
			
		||||
use Symfony\Component\Translation\MessageCatalogue;
 | 
			
		||||
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
 | 
			
		||||
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
 | 
			
		||||
use Symfony\Component\Translation\Writer\TranslationWriterInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author Mathieu Santostefano <msantostefano@protonmail.com>
 | 
			
		||||
 */
 | 
			
		||||
#[AsCommand(name: 'translation:pull', description: 'Pull translations from a given provider.')]
 | 
			
		||||
final class TranslationPullCommand extends Command
 | 
			
		||||
{
 | 
			
		||||
    use TranslationTrait;
 | 
			
		||||
 | 
			
		||||
    private TranslationProviderCollection $providerCollection;
 | 
			
		||||
    private TranslationWriterInterface $writer;
 | 
			
		||||
    private TranslationReaderInterface $reader;
 | 
			
		||||
    private string $defaultLocale;
 | 
			
		||||
    private array $transPaths;
 | 
			
		||||
    private array $enabledLocales;
 | 
			
		||||
 | 
			
		||||
    public function __construct(TranslationProviderCollection $providerCollection, TranslationWriterInterface $writer, TranslationReaderInterface $reader, string $defaultLocale, array $transPaths = [], array $enabledLocales = [])
 | 
			
		||||
    {
 | 
			
		||||
        $this->providerCollection = $providerCollection;
 | 
			
		||||
        $this->writer = $writer;
 | 
			
		||||
        $this->reader = $reader;
 | 
			
		||||
        $this->defaultLocale = $defaultLocale;
 | 
			
		||||
        $this->transPaths = $transPaths;
 | 
			
		||||
        $this->enabledLocales = $enabledLocales;
 | 
			
		||||
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($input->mustSuggestArgumentValuesFor('provider')) {
 | 
			
		||||
            $suggestions->suggestValues($this->providerCollection->keys());
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($input->mustSuggestOptionValuesFor('domains')) {
 | 
			
		||||
            $provider = $this->providerCollection->get($input->getArgument('provider'));
 | 
			
		||||
 | 
			
		||||
            if (method_exists($provider, 'getDomains')) {
 | 
			
		||||
                $suggestions->suggestValues($provider->getDomains());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($input->mustSuggestOptionValuesFor('locales')) {
 | 
			
		||||
            $suggestions->suggestValues($this->enabledLocales);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($input->mustSuggestOptionValuesFor('format')) {
 | 
			
		||||
            $suggestions->suggestValues(['php', 'xlf', 'xlf12', 'xlf20', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'json', 'ini', 'res']);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function configure(): void
 | 
			
		||||
    {
 | 
			
		||||
        $keys = $this->providerCollection->keys();
 | 
			
		||||
        $defaultProvider = 1 === \count($keys) ? $keys[0] : null;
 | 
			
		||||
 | 
			
		||||
        $this
 | 
			
		||||
            ->setDefinition([
 | 
			
		||||
                new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to pull translations from.', $defaultProvider),
 | 
			
		||||
                new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with provider ones (it will delete not synchronized messages).'),
 | 
			
		||||
                new InputOption('intl-icu', null, InputOption::VALUE_NONE, 'Associated to --force option, it will write messages in "%domain%+intl-icu.%locale%.xlf" files.'),
 | 
			
		||||
                new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'),
 | 
			
		||||
                new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'),
 | 
			
		||||
                new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format.', 'xlf12'),
 | 
			
		||||
                new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Write messages as a tree-like structure. Needs --format=yaml. The given value defines the level where to switch to inline YAML'),
 | 
			
		||||
            ])
 | 
			
		||||
            ->setHelp(<<<'EOF'
 | 
			
		||||
The <info>%command.name%</> command pulls translations from the given provider. Only
 | 
			
		||||
new translations are pulled, existing ones are not overwritten.
 | 
			
		||||
 | 
			
		||||
You can overwrite existing translations (and remove the missing ones on local side) by using the <comment>--force</> flag:
 | 
			
		||||
 | 
			
		||||
  <info>php %command.full_name% --force provider</>
 | 
			
		||||
 | 
			
		||||
Full example:
 | 
			
		||||
 | 
			
		||||
  <info>php %command.full_name% provider --force --domains=messages --domains=validators --locales=en</>
 | 
			
		||||
 | 
			
		||||
This command pulls all translations associated with the <comment>messages</> and <comment>validators</> domains for the <comment>en</> locale.
 | 
			
		||||
Local translations for the specified domains and locale are deleted if they're not present on the provider and overwritten if it's the case.
 | 
			
		||||
Local translations for others domains and locales are ignored.
 | 
			
		||||
EOF
 | 
			
		||||
            )
 | 
			
		||||
        ;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function execute(InputInterface $input, OutputInterface $output): int
 | 
			
		||||
    {
 | 
			
		||||
        $io = new SymfonyStyle($input, $output);
 | 
			
		||||
 | 
			
		||||
        $provider = $this->providerCollection->get($input->getArgument('provider'));
 | 
			
		||||
        $force = $input->getOption('force');
 | 
			
		||||
        $intlIcu = $input->getOption('intl-icu');
 | 
			
		||||
        $locales = $input->getOption('locales') ?: $this->enabledLocales;
 | 
			
		||||
        $domains = $input->getOption('domains');
 | 
			
		||||
        $format = $input->getOption('format');
 | 
			
		||||
        $asTree = (int) $input->getOption('as-tree');
 | 
			
		||||
        $xliffVersion = '1.2';
 | 
			
		||||
 | 
			
		||||
        if ($intlIcu && !$force) {
 | 
			
		||||
            $io->note('--intl-icu option only has an effect when used with --force. Here, it will be ignored.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        switch ($format) {
 | 
			
		||||
            case 'xlf20': $xliffVersion = '2.0';
 | 
			
		||||
                // no break
 | 
			
		||||
            case 'xlf12': $format = 'xlf';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $writeOptions = [
 | 
			
		||||
            'path' => end($this->transPaths),
 | 
			
		||||
            'xliff_version' => $xliffVersion,
 | 
			
		||||
            'default_locale' => $this->defaultLocale,
 | 
			
		||||
            'as_tree' => (bool) $asTree,
 | 
			
		||||
            'inline' => $asTree,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        if (!$domains) {
 | 
			
		||||
            $domains = $provider->getDomains();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $providerTranslations = $provider->read($domains, $locales);
 | 
			
		||||
 | 
			
		||||
        if ($force) {
 | 
			
		||||
            foreach ($providerTranslations->getCatalogues() as $catalogue) {
 | 
			
		||||
                $operation = new TargetOperation(new MessageCatalogue($catalogue->getLocale()), $catalogue);
 | 
			
		||||
                if ($intlIcu) {
 | 
			
		||||
                    $operation->moveMessagesToIntlDomainsIfPossible();
 | 
			
		||||
                }
 | 
			
		||||
                $this->writer->write($operation->getResult(), $format, $writeOptions);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $io->success(sprintf('Local translations has been updated from "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
 | 
			
		||||
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths);
 | 
			
		||||
 | 
			
		||||
        // Append pulled translations to local ones.
 | 
			
		||||
        $localTranslations->addBag($providerTranslations->diff($localTranslations));
 | 
			
		||||
 | 
			
		||||
        foreach ($localTranslations->getCatalogues() as $catalogue) {
 | 
			
		||||
            $this->writer->write($catalogue, $format, $writeOptions);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $io->success(sprintf('New translations from "%s" has been written locally (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
 | 
			
		||||
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										182
									
								
								vendor/symfony/translation/Command/TranslationPushCommand.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								vendor/symfony/translation/Command/TranslationPushCommand.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,182 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of the Symfony package.
 | 
			
		||||
 *
 | 
			
		||||
 * (c) Fabien Potencier <fabien@symfony.com>
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view the LICENSE
 | 
			
		||||
 * file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Symfony\Component\Translation\Command;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Console\Attribute\AsCommand;
 | 
			
		||||
use Symfony\Component\Console\Command\Command;
 | 
			
		||||
use Symfony\Component\Console\Completion\CompletionInput;
 | 
			
		||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
 | 
			
		||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
 | 
			
		||||
use Symfony\Component\Console\Input\InputArgument;
 | 
			
		||||
use Symfony\Component\Console\Input\InputInterface;
 | 
			
		||||
use Symfony\Component\Console\Input\InputOption;
 | 
			
		||||
use Symfony\Component\Console\Output\OutputInterface;
 | 
			
		||||
use Symfony\Component\Console\Style\SymfonyStyle;
 | 
			
		||||
use Symfony\Component\Translation\Provider\FilteringProvider;
 | 
			
		||||
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
 | 
			
		||||
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
 | 
			
		||||
use Symfony\Component\Translation\TranslatorBag;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author Mathieu Santostefano <msantostefano@protonmail.com>
 | 
			
		||||
 */
 | 
			
		||||
#[AsCommand(name: 'translation:push', description: 'Push translations to a given provider.')]
 | 
			
		||||
final class TranslationPushCommand extends Command
 | 
			
		||||
{
 | 
			
		||||
    use TranslationTrait;
 | 
			
		||||
 | 
			
		||||
    private TranslationProviderCollection $providers;
 | 
			
		||||
    private TranslationReaderInterface $reader;
 | 
			
		||||
    private array $transPaths;
 | 
			
		||||
    private array $enabledLocales;
 | 
			
		||||
 | 
			
		||||
    public function __construct(TranslationProviderCollection $providers, TranslationReaderInterface $reader, array $transPaths = [], array $enabledLocales = [])
 | 
			
		||||
    {
 | 
			
		||||
        $this->providers = $providers;
 | 
			
		||||
        $this->reader = $reader;
 | 
			
		||||
        $this->transPaths = $transPaths;
 | 
			
		||||
        $this->enabledLocales = $enabledLocales;
 | 
			
		||||
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($input->mustSuggestArgumentValuesFor('provider')) {
 | 
			
		||||
            $suggestions->suggestValues($this->providers->keys());
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($input->mustSuggestOptionValuesFor('domains')) {
 | 
			
		||||
            $provider = $this->providers->get($input->getArgument('provider'));
 | 
			
		||||
 | 
			
		||||
            if ($provider && method_exists($provider, 'getDomains')) {
 | 
			
		||||
                $domains = $provider->getDomains();
 | 
			
		||||
                $suggestions->suggestValues($domains);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($input->mustSuggestOptionValuesFor('locales')) {
 | 
			
		||||
            $suggestions->suggestValues($this->enabledLocales);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function configure(): void
 | 
			
		||||
    {
 | 
			
		||||
        $keys = $this->providers->keys();
 | 
			
		||||
        $defaultProvider = 1 === \count($keys) ? $keys[0] : null;
 | 
			
		||||
 | 
			
		||||
        $this
 | 
			
		||||
            ->setDefinition([
 | 
			
		||||
                new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to push translations to.', $defaultProvider),
 | 
			
		||||
                new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with local ones (it will delete not synchronized messages).'),
 | 
			
		||||
                new InputOption('delete-missing', null, InputOption::VALUE_NONE, 'Delete translations available on provider but not locally.'),
 | 
			
		||||
                new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'),
 | 
			
		||||
                new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales),
 | 
			
		||||
            ])
 | 
			
		||||
            ->setHelp(<<<'EOF'
 | 
			
		||||
The <info>%command.name%</> command pushes translations to the given provider. Only new
 | 
			
		||||
translations are pushed, existing ones are not overwritten.
 | 
			
		||||
 | 
			
		||||
You can overwrite existing translations by using the <comment>--force</> flag:
 | 
			
		||||
 | 
			
		||||
  <info>php %command.full_name% --force provider</>
 | 
			
		||||
 | 
			
		||||
You can delete provider translations which are not present locally by using the <comment>--delete-missing</> flag:
 | 
			
		||||
 | 
			
		||||
  <info>php %command.full_name% --delete-missing provider</>
 | 
			
		||||
 | 
			
		||||
Full example:
 | 
			
		||||
 | 
			
		||||
  <info>php %command.full_name% provider --force --delete-missing --domains=messages --domains=validators --locales=en</>
 | 
			
		||||
 | 
			
		||||
This command pushes all translations associated with the <comment>messages</> and <comment>validators</> domains for the <comment>en</> locale.
 | 
			
		||||
Provider translations for the specified domains and locale are deleted if they're not present locally and overwritten if it's the case.
 | 
			
		||||
Provider translations for others domains and locales are ignored.
 | 
			
		||||
EOF
 | 
			
		||||
            )
 | 
			
		||||
        ;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function execute(InputInterface $input, OutputInterface $output): int
 | 
			
		||||
    {
 | 
			
		||||
        $provider = $this->providers->get($input->getArgument('provider'));
 | 
			
		||||
 | 
			
		||||
        if (!$this->enabledLocales) {
 | 
			
		||||
            throw new InvalidArgumentException(sprintf('You must define "framework.enabled_locales" or "framework.translator.providers.%s.locales" config key in order to work with translation providers.', parse_url($provider, \PHP_URL_SCHEME)));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $io = new SymfonyStyle($input, $output);
 | 
			
		||||
        $domains = $input->getOption('domains');
 | 
			
		||||
        $locales = $input->getOption('locales');
 | 
			
		||||
        $force = $input->getOption('force');
 | 
			
		||||
        $deleteMissing = $input->getOption('delete-missing');
 | 
			
		||||
 | 
			
		||||
        if (!$domains && $provider instanceof FilteringProvider) {
 | 
			
		||||
            $domains = $provider->getDomains();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Reading local translations must be done after retrieving the domains from the provider
 | 
			
		||||
        // in order to manage only translations from configured domains
 | 
			
		||||
        $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths);
 | 
			
		||||
 | 
			
		||||
        if (!$domains) {
 | 
			
		||||
            $domains = $this->getDomainsFromTranslatorBag($localTranslations);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!$deleteMissing && $force) {
 | 
			
		||||
            $provider->write($localTranslations);
 | 
			
		||||
 | 
			
		||||
            $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)));
 | 
			
		||||
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $providerTranslations = $provider->read($domains, $locales);
 | 
			
		||||
 | 
			
		||||
        if ($deleteMissing) {
 | 
			
		||||
            $provider->delete($providerTranslations->diff($localTranslations));
 | 
			
		||||
 | 
			
		||||
            $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)));
 | 
			
		||||
 | 
			
		||||
            // Read provider translations again, after missing translations deletion,
 | 
			
		||||
            // to avoid push freshly deleted translations.
 | 
			
		||||
            $providerTranslations = $provider->read($domains, $locales);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $translationsToWrite = $localTranslations->diff($providerTranslations);
 | 
			
		||||
 | 
			
		||||
        if ($force) {
 | 
			
		||||
            $translationsToWrite->addBag($localTranslations->intersect($providerTranslations));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $provider->write($translationsToWrite);
 | 
			
		||||
 | 
			
		||||
        $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)));
 | 
			
		||||
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getDomainsFromTranslatorBag(TranslatorBag $translatorBag): array
 | 
			
		||||
    {
 | 
			
		||||
        $domains = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($translatorBag->getCatalogues() as $catalogue) {
 | 
			
		||||
            $domains += $catalogue->getDomains();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return array_unique($domains);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										77
									
								
								vendor/symfony/translation/Command/TranslationTrait.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								vendor/symfony/translation/Command/TranslationTrait.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of the Symfony package.
 | 
			
		||||
 *
 | 
			
		||||
 * (c) Fabien Potencier <fabien@symfony.com>
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view the LICENSE
 | 
			
		||||
 * file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Symfony\Component\Translation\Command;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Translation\MessageCatalogue;
 | 
			
		||||
use Symfony\Component\Translation\MessageCatalogueInterface;
 | 
			
		||||
use Symfony\Component\Translation\TranslatorBag;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 */
 | 
			
		||||
trait TranslationTrait
 | 
			
		||||
{
 | 
			
		||||
    private function readLocalTranslations(array $locales, array $domains, array $transPaths): TranslatorBag
 | 
			
		||||
    {
 | 
			
		||||
        $bag = new TranslatorBag();
 | 
			
		||||
 | 
			
		||||
        foreach ($locales as $locale) {
 | 
			
		||||
            $catalogue = new MessageCatalogue($locale);
 | 
			
		||||
            foreach ($transPaths as $path) {
 | 
			
		||||
                $this->reader->read($path, $catalogue);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ($domains) {
 | 
			
		||||
                foreach ($domains as $domain) {
 | 
			
		||||
                    $bag->addCatalogue($this->filterCatalogue($catalogue, $domain));
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                $bag->addCatalogue($catalogue);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $bag;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue
 | 
			
		||||
    {
 | 
			
		||||
        $filteredCatalogue = new MessageCatalogue($catalogue->getLocale());
 | 
			
		||||
 | 
			
		||||
        // extract intl-icu messages only
 | 
			
		||||
        $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
 | 
			
		||||
        if ($intlMessages = $catalogue->all($intlDomain)) {
 | 
			
		||||
            $filteredCatalogue->add($intlMessages, $intlDomain);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // extract all messages and subtract intl-icu messages
 | 
			
		||||
        if ($messages = array_diff($catalogue->all($domain), $intlMessages)) {
 | 
			
		||||
            $filteredCatalogue->add($messages, $domain);
 | 
			
		||||
        }
 | 
			
		||||
        foreach ($catalogue->getResources() as $resource) {
 | 
			
		||||
            $filteredCatalogue->addResource($resource);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($metadata = $catalogue->getMetadata('', $intlDomain)) {
 | 
			
		||||
            foreach ($metadata as $k => $v) {
 | 
			
		||||
                $filteredCatalogue->setMetadata($k, $v, $intlDomain);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($metadata = $catalogue->getMetadata('', $domain)) {
 | 
			
		||||
            foreach ($metadata as $k => $v) {
 | 
			
		||||
                $filteredCatalogue->setMetadata($k, $v, $domain);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $filteredCatalogue;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										285
									
								
								vendor/symfony/translation/Command/XliffLintCommand.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								vendor/symfony/translation/Command/XliffLintCommand.php
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,285 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of the Symfony package.
 | 
			
		||||
 *
 | 
			
		||||
 * (c) Fabien Potencier <fabien@symfony.com>
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view the LICENSE
 | 
			
		||||
 * file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Symfony\Component\Translation\Command;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Console\Attribute\AsCommand;
 | 
			
		||||
use Symfony\Component\Console\CI\GithubActionReporter;
 | 
			
		||||
use Symfony\Component\Console\Command\Command;
 | 
			
		||||
use Symfony\Component\Console\Completion\CompletionInput;
 | 
			
		||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
 | 
			
		||||
use Symfony\Component\Console\Exception\RuntimeException;
 | 
			
		||||
use Symfony\Component\Console\Input\InputArgument;
 | 
			
		||||
use Symfony\Component\Console\Input\InputInterface;
 | 
			
		||||
use Symfony\Component\Console\Input\InputOption;
 | 
			
		||||
use Symfony\Component\Console\Output\OutputInterface;
 | 
			
		||||
use Symfony\Component\Console\Style\SymfonyStyle;
 | 
			
		||||
use Symfony\Component\Translation\Exception\InvalidArgumentException;
 | 
			
		||||
use Symfony\Component\Translation\Util\XliffUtils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Validates XLIFF files syntax and outputs encountered errors.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Grégoire Pineau <lyrixx@lyrixx.info>
 | 
			
		||||
 * @author Robin Chalas <robin.chalas@gmail.com>
 | 
			
		||||
 * @author Javier Eguiluz <javier.eguiluz@gmail.com>
 | 
			
		||||
 */
 | 
			
		||||
#[AsCommand(name: 'lint:xliff', description: 'Lint an XLIFF file and outputs encountered errors')]
 | 
			
		||||
class XliffLintCommand extends Command
 | 
			
		||||
{
 | 
			
		||||
    private string $format;
 | 
			
		||||
    private bool $displayCorrectFiles;
 | 
			
		||||
    private ?\Closure $directoryIteratorProvider;
 | 
			
		||||
    private ?\Closure $isReadableProvider;
 | 
			
		||||
    private bool $requireStrictFileNames;
 | 
			
		||||
 | 
			
		||||
    public function __construct(?string $name = null, ?callable $directoryIteratorProvider = null, ?callable $isReadableProvider = null, bool $requireStrictFileNames = true)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct($name);
 | 
			
		||||
 | 
			
		||||
        $this->directoryIteratorProvider = null === $directoryIteratorProvider ? null : $directoryIteratorProvider(...);
 | 
			
		||||
        $this->isReadableProvider = null === $isReadableProvider ? null : $isReadableProvider(...);
 | 
			
		||||
        $this->requireStrictFileNames = $requireStrictFileNames;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    protected function configure()
 | 
			
		||||
    {
 | 
			
		||||
        $this
 | 
			
		||||
            ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN')
 | 
			
		||||
            ->addOption('format', null, InputOption::VALUE_REQUIRED, sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())))
 | 
			
		||||
            ->setHelp(<<<EOF
 | 
			
		||||
The <info>%command.name%</info> command lints an XLIFF file and outputs to STDOUT
 | 
			
		||||
the first encountered syntax error.
 | 
			
		||||
 | 
			
		||||
You can validates XLIFF contents passed from STDIN:
 | 
			
		||||
 | 
			
		||||
  <info>cat filename | php %command.full_name% -</info>
 | 
			
		||||
 | 
			
		||||
You can also validate the syntax of a file:
 | 
			
		||||
 | 
			
		||||
  <info>php %command.full_name% filename</info>
 | 
			
		||||
 | 
			
		||||
Or of a whole directory:
 | 
			
		||||
 | 
			
		||||
  <info>php %command.full_name% dirname</info>
 | 
			
		||||
  <info>php %command.full_name% dirname --format=json</info>
 | 
			
		||||
 | 
			
		||||
EOF
 | 
			
		||||
            )
 | 
			
		||||
        ;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function execute(InputInterface $input, OutputInterface $output): int
 | 
			
		||||
    {
 | 
			
		||||
        $io = new SymfonyStyle($input, $output);
 | 
			
		||||
        $filenames = (array) $input->getArgument('filename');
 | 
			
		||||
        $this->format = $input->getOption('format') ?? (GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt');
 | 
			
		||||
        $this->displayCorrectFiles = $output->isVerbose();
 | 
			
		||||
 | 
			
		||||
        if (['-'] === $filenames) {
 | 
			
		||||
            return $this->display($io, [$this->validate(file_get_contents('php://stdin'))]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!$filenames) {
 | 
			
		||||
            throw new RuntimeException('Please provide a filename or pipe file content to STDIN.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $filesInfo = [];
 | 
			
		||||
        foreach ($filenames as $filename) {
 | 
			
		||||
            if (!$this->isReadable($filename)) {
 | 
			
		||||
                throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            foreach ($this->getFiles($filename) as $file) {
 | 
			
		||||
                $filesInfo[] = $this->validate(file_get_contents($file), $file);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->display($io, $filesInfo);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function validate(string $content, ?string $file = null): array
 | 
			
		||||
    {
 | 
			
		||||
        $errors = [];
 | 
			
		||||
 | 
			
		||||
        // Avoid: Warning DOMDocument::loadXML(): Empty string supplied as input
 | 
			
		||||
        if ('' === trim($content)) {
 | 
			
		||||
            return ['file' => $file, 'valid' => true];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $internal = libxml_use_internal_errors(true);
 | 
			
		||||
 | 
			
		||||
        $document = new \DOMDocument();
 | 
			
		||||
        $document->loadXML($content);
 | 
			
		||||
 | 
			
		||||
        if (null !== $targetLanguage = $this->getTargetLanguageFromFile($document)) {
 | 
			
		||||
            $normalizedLocalePattern = sprintf('(%s|%s)', preg_quote($targetLanguage, '/'), preg_quote(str_replace('-', '_', $targetLanguage), '/'));
 | 
			
		||||
            // strict file names require translation files to be named '____.locale.xlf'
 | 
			
		||||
            // otherwise, both '____.locale.xlf' and 'locale.____.xlf' are allowed
 | 
			
		||||
            // also, the regexp matching must be case-insensitive, as defined for 'target-language' values
 | 
			
		||||
            // http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html#target-language
 | 
			
		||||
            $expectedFilenamePattern = $this->requireStrictFileNames ? sprintf('/^.*\.(?i:%s)\.(?:xlf|xliff)/', $normalizedLocalePattern) : sprintf('/^(?:.*\.(?i:%s)|(?i:%s)\..*)\.(?:xlf|xliff)/', $normalizedLocalePattern, $normalizedLocalePattern);
 | 
			
		||||
 | 
			
		||||
            if (0 === preg_match($expectedFilenamePattern, basename($file))) {
 | 
			
		||||
                $errors[] = [
 | 
			
		||||
                    'line' => -1,
 | 
			
		||||
                    'column' => -1,
 | 
			
		||||
                    'message' => sprintf('There is a mismatch between the language included in the file name ("%s") and the "%s" value used in the "target-language" attribute of the file.', basename($file), $targetLanguage),
 | 
			
		||||
                ];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach (XliffUtils::validateSchema($document) as $xmlError) {
 | 
			
		||||
            $errors[] = [
 | 
			
		||||
                'line' => $xmlError['line'],
 | 
			
		||||
                'column' => $xmlError['column'],
 | 
			
		||||
                'message' => $xmlError['message'],
 | 
			
		||||
            ];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        libxml_clear_errors();
 | 
			
		||||
        libxml_use_internal_errors($internal);
 | 
			
		||||
 | 
			
		||||
        return ['file' => $file, 'valid' => 0 === \count($errors), 'messages' => $errors];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function display(SymfonyStyle $io, array $files): int
 | 
			
		||||
    {
 | 
			
		||||
        return match ($this->format) {
 | 
			
		||||
            'txt' => $this->displayTxt($io, $files),
 | 
			
		||||
            'json' => $this->displayJson($io, $files),
 | 
			
		||||
            'github' => $this->displayTxt($io, $files, true),
 | 
			
		||||
            default => throw new InvalidArgumentException(sprintf('Supported formats are "%s".', implode('", "', $this->getAvailableFormatOptions()))),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function displayTxt(SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false): int
 | 
			
		||||
    {
 | 
			
		||||
        $countFiles = \count($filesInfo);
 | 
			
		||||
        $erroredFiles = 0;
 | 
			
		||||
        $githubReporter = $errorAsGithubAnnotations ? new GithubActionReporter($io) : null;
 | 
			
		||||
 | 
			
		||||
        foreach ($filesInfo as $info) {
 | 
			
		||||
            if ($info['valid'] && $this->displayCorrectFiles) {
 | 
			
		||||
                $io->comment('<info>OK</info>'.($info['file'] ? sprintf(' in %s', $info['file']) : ''));
 | 
			
		||||
            } elseif (!$info['valid']) {
 | 
			
		||||
                ++$erroredFiles;
 | 
			
		||||
                $io->text('<error> ERROR </error>'.($info['file'] ? sprintf(' in %s', $info['file']) : ''));
 | 
			
		||||
                $io->listing(array_map(function ($error) use ($info, $githubReporter) {
 | 
			
		||||
                    // general document errors have a '-1' line number
 | 
			
		||||
                    $line = -1 === $error['line'] ? null : $error['line'];
 | 
			
		||||
 | 
			
		||||
                    $githubReporter?->error($error['message'], $info['file'], $line, null !== $line ? $error['column'] : null);
 | 
			
		||||
 | 
			
		||||
                    return null === $line ? $error['message'] : sprintf('Line %d, Column %d: %s', $line, $error['column'], $error['message']);
 | 
			
		||||
                }, $info['messages']));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (0 === $erroredFiles) {
 | 
			
		||||
            $io->success(sprintf('All %d XLIFF files contain valid syntax.', $countFiles));
 | 
			
		||||
        } else {
 | 
			
		||||
            $io->warning(sprintf('%d XLIFF files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return min($erroredFiles, 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function displayJson(SymfonyStyle $io, array $filesInfo): int
 | 
			
		||||
    {
 | 
			
		||||
        $errors = 0;
 | 
			
		||||
 | 
			
		||||
        array_walk($filesInfo, function (&$v) use (&$errors) {
 | 
			
		||||
            $v['file'] = (string) $v['file'];
 | 
			
		||||
            if (!$v['valid']) {
 | 
			
		||||
                ++$errors;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $io->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));
 | 
			
		||||
 | 
			
		||||
        return min($errors, 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return iterable<\SplFileInfo>
 | 
			
		||||
     */
 | 
			
		||||
    private function getFiles(string $fileOrDirectory): iterable
 | 
			
		||||
    {
 | 
			
		||||
        if (is_file($fileOrDirectory)) {
 | 
			
		||||
            yield new \SplFileInfo($fileOrDirectory);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) {
 | 
			
		||||
            if (!\in_array($file->getExtension(), ['xlf', 'xliff'])) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            yield $file;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return iterable<\SplFileInfo>
 | 
			
		||||
     */
 | 
			
		||||
    private function getDirectoryIterator(string $directory): iterable
 | 
			
		||||
    {
 | 
			
		||||
        $default = fn ($directory) => new \RecursiveIteratorIterator(
 | 
			
		||||
            new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
 | 
			
		||||
            \RecursiveIteratorIterator::LEAVES_ONLY
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (null !== $this->directoryIteratorProvider) {
 | 
			
		||||
            return ($this->directoryIteratorProvider)($directory, $default);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $default($directory);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function isReadable(string $fileOrDirectory): bool
 | 
			
		||||
    {
 | 
			
		||||
        $default = fn ($fileOrDirectory) => is_readable($fileOrDirectory);
 | 
			
		||||
 | 
			
		||||
        if (null !== $this->isReadableProvider) {
 | 
			
		||||
            return ($this->isReadableProvider)($fileOrDirectory, $default);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $default($fileOrDirectory);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getTargetLanguageFromFile(\DOMDocument $xliffContents): ?string
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($xliffContents->getElementsByTagName('file')[0]->attributes ?? [] as $attribute) {
 | 
			
		||||
            if ('target-language' === $attribute->nodeName) {
 | 
			
		||||
                return $attribute->nodeValue;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($input->mustSuggestOptionValuesFor('format')) {
 | 
			
		||||
            $suggestions->suggestValues($this->getAvailableFormatOptions());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getAvailableFormatOptions(): array
 | 
			
		||||
    {
 | 
			
		||||
        return ['txt', 'json', 'github'];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user