├── src
├── Translator
│ └── Extractor
│ │ ├── ExtractorInterface.php
│ │ ├── ChainExtractor.php
│ │ ├── SymfonyExtractor.php
│ │ └── JsExtractor.php
├── IncenteevTranslationCheckerBundle.php
├── DependencyInjection
│ ├── Compiler
│ │ ├── ExtractorPass.php
│ │ └── ConfigureTranslatorPass.php
│ ├── Configuration.php
│ └── IncenteevTranslationCheckerExtension.php
├── Resources
│ └── config
│ │ └── services.xml
└── Command
│ ├── FindMissingCommand.php
│ └── CompareCommand.php
├── phpstan.neon
├── LICENSE
├── Changelog.md
├── composer.json
└── README.md
/src/Translator/Extractor/ExtractorInterface.php:
--------------------------------------------------------------------------------
1 | addCompilerPass(new ConfigureTranslatorPass());
18 | $container->addCompilerPass(new ExtractorPass());
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Translator/Extractor/ChainExtractor.php:
--------------------------------------------------------------------------------
1 | extractors = $extractors;
20 | }
21 |
22 | public function extract(MessageCatalogue $catalogue)
23 | {
24 | foreach ($this->extractors as $extractor) {
25 | $extractor->extract($catalogue);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 8
3 | paths:
4 | - src/
5 | - tests/
6 | ignoreErrors:
7 | - '#^Method Incenteev\\TranslationCheckerBundle\\Tests\\[^:]++\:\:test\w++\(\) has no return type specified\.$#'
8 | - '#^Method Incenteev\\TranslationCheckerBundle\\Tests\\[^:]++\:\:provide\w++\(\) return type has no value type specified in iterable type iterable\.$#'
9 | symfony:
10 | consoleApplicationLoader: tests/FixtureApp/console.php
11 |
12 | includes:
13 | - phar://phpstan.phar/conf/bleedingEdge.neon
14 | - vendor/phpstan/phpstan-phpunit/extension.neon
15 | - vendor/phpstan/phpstan-phpunit/rules.neon
16 | - vendor/phpstan/phpstan-symfony/extension.neon
17 | - vendor/jangregor/phpstan-prophecy/extension.neon
18 |
--------------------------------------------------------------------------------
/src/Translator/Extractor/SymfonyExtractor.php:
--------------------------------------------------------------------------------
1 | extractor = $extractor;
22 | $this->paths = $paths;
23 | }
24 |
25 | public function extract(MessageCatalogue $catalogue): void
26 | {
27 | foreach ($this->paths as $path) {
28 | if (!is_dir($path)) {
29 | continue;
30 | }
31 |
32 | $this->extractor->extract($path, $catalogue);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 Incenteev
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Compiler/ExtractorPass.php:
--------------------------------------------------------------------------------
1 | findTaggedServiceIds('incenteev_translation_checker.extractor') as $id => $tags) {
20 | $extractors[] = new Reference($id);
21 | }
22 |
23 | // If there is only one configured extractor, skip the chain one
24 | if (1 === count($extractors)) {
25 | $container->setAlias('incenteev_translation_checker.extractor', new Alias((string) $extractors[0], $container->getAlias('incenteev_translation_checker.extractor')->isPublic()));
26 |
27 | return;
28 | }
29 |
30 | $container->getDefinition('incenteev_translation_checker.extractor.chain')->replaceArgument(0, $extractors);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Compiler/ConfigureTranslatorPass.php:
--------------------------------------------------------------------------------
1 | hasDefinition('incenteev_translation_checker.exposing_translator')) {
16 | return;
17 | }
18 |
19 | if (!$container->has('translator.default')) {
20 | return;
21 | }
22 |
23 | $translatorDef = $container->findDefinition('translator.default');
24 |
25 | $optionsArgumentIndex = 4;
26 |
27 | $options = $translatorDef->getArgument($optionsArgumentIndex);
28 |
29 | if (!is_array($options)) {
30 | // Weird setup. Reset all options
31 | $options = array();
32 | }
33 |
34 | // use a separate cache as we have no fallback locales
35 | $options['cache_dir'] = '%kernel.cache_dir%/incenteev_translations';
36 |
37 | $container->findDefinition('incenteev_translation_checker.exposing_translator')
38 | ->replaceArgument($optionsArgumentIndex, $options);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Changelog.md:
--------------------------------------------------------------------------------
1 | # 1.5.0 (2020-03-17)
2 |
3 | * Added support for Symfony 5
4 | * Dropped support for unmaintained Symfony and PHP versions.
5 |
6 | # 1.4.0 (2018-11-23)
7 |
8 | Features:
9 |
10 | * Added the `--whitelist-file` option to allow providing a whitelist of keys that should not be considered missing or obsolete.
11 |
12 | # 1.3.1 (2018-02-20)
13 |
14 | * Added support for Symfony 4
15 |
16 | # 1.3.0 (2017-08-04)
17 |
18 | * Added support for chaining multiple extractors. New extractors can be registered using the `incenteev_translation_checker.extractor` tag
19 | * Added support for autoconfiguration for custom extractors in Symfony 3.3+ (adding the tag implicitly)
20 | * Added a JS extractor, to extract translation from JS files when using willdurand/js-translation-bundle. See https://github.com/Incenteev/IncenteevTranslationCheckerBundle/pull/18 for current limitations
21 | * Removed tests and development files from the ZIP archive to make the download smaller.
22 |
23 | ## 1.2.1 (2017-06-12)
24 |
25 | * Fixed compatibility with Symfony 3.3+
26 |
27 | ## 1.2.0 (2017-02-21)
28 |
29 | * Added support for Symfony 3
30 | * Dropped support for Symfony 2.7 and older
31 |
32 | ## 1.1.0 (2015-09-29)
33 |
34 | Features:
35 |
36 | * Added the `--only-obsolete` flag in `incenteev:translation:compare` to check only obsolete keys
37 |
38 | Bugfix:
39 |
40 | * Fixed compatibility with Symfony 2.7+
41 |
42 | ## 1.0.0 (2015-06-08)
43 |
44 | Initial stable release
45 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 | getRootNode();
17 |
18 | $rootNode
19 | ->children()
20 | ->arrayNode('extraction')
21 | ->addDefaultsIfNotSet()
22 | ->fixXmlConfig('bundle')
23 | ->children()
24 | ->arrayNode('bundles')
25 | ->prototype('scalar')->end()
26 | ->end()
27 | ->arrayNode('js')
28 | ->addDefaultsIfNotSet()
29 | ->fixXmlConfig('path')
30 | ->children()
31 | ->scalarNode('default_domain')
32 | ->info('The default domain used for JS translation (should match the JsTranslationBundle config)')
33 | ->defaultValue('messages')
34 | ->end()
35 | ->arrayNode('paths')
36 | ->prototype('scalar')->end()
37 | ->end()
38 | ->end()
39 | ->end()
40 | ->end()
41 | ->end()
42 | ->end();
43 |
44 | return $treeBuilder;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "incenteev/translation-checker-bundle",
3 | "type": "symfony-bundle",
4 | "description": "CLI tools to check translations in a Symfony project",
5 | "keywords": ["translation check", "translation", "testing"],
6 | "homepage": "https://github.com/Incenteev/translation-checker-bundle",
7 | "license": "MIT",
8 | "authors": [
9 | {
10 | "name": "Christophe Coevoet",
11 | "email": "stof@notk.org"
12 | }
13 | ],
14 | "require": {
15 | "php": "^7.4 || ^8.0",
16 | "symfony/framework-bundle": "^4.4.29 || ^5.3.4 || ^6.0.0 || ^7.0.0",
17 | "symfony/console": "^4.4.29 || ^5.3.4 || ^6.0.0 || ^7.0.0",
18 | "symfony/config": "^4.4.29 || ^5.3.4 || ^6.0.0 || ^7.0.0",
19 | "symfony/dependency-injection": "^4.4.29 || ^5.3.4 || ^6.0.0 || ^7.0.0",
20 | "symfony/finder": "^4.4.29 || ^5.3.4 || ^6.0.0 || ^7.0.0",
21 | "symfony/http-kernel": "^4.4.29 || ^5.3.4 || ^6.0.0 || ^7.0.0",
22 | "symfony/translation": "^4.4.32 || ^5.3.4 || ^6.0.0 || ^7.0.0",
23 | "symfony/yaml": "^4.4.29 || ^5.3.4 || ^6.0.0 || ^7.0.0"
24 | },
25 | "require-dev": {
26 | "jangregor/phpstan-prophecy": "^1.0",
27 | "phpspec/prophecy-phpunit": "^2.0",
28 | "phpstan/phpstan": "^1.10",
29 | "phpstan/phpstan-phpunit": "^1.3",
30 | "phpstan/phpstan-symfony": "^1.3",
31 | "phpunit/phpunit": "^9.6.15",
32 | "symfony/phpunit-bridge": "^6.4.0 || ^7.0.0"
33 | },
34 | "autoload": {
35 | "psr-4": { "Incenteev\\TranslationCheckerBundle\\": "src" }
36 | },
37 | "autoload-dev": {
38 | "psr-4": { "Incenteev\\TranslationCheckerBundle\\Tests\\": "tests" }
39 | },
40 | "extra": {
41 | "branch-alias": {
42 | "dev-master": "1.x-dev"
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Resources/config/services.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
25 |
26 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/Translator/Extractor/JsExtractor.php:
--------------------------------------------------------------------------------
1 | paths = $paths;
22 | $this->defaultDomain = $defaultDomain;
23 | }
24 |
25 | public function extract(MessageCatalogue $catalogue): void
26 | {
27 | $directories = array();
28 |
29 | foreach ($this->paths as $path) {
30 | if (is_dir($path)) {
31 | $directories[] = $path;
32 |
33 | continue;
34 | }
35 |
36 | if (is_file($path)) {
37 | $contents = file_get_contents($path);
38 | if ($contents === false) {
39 | throw new \RuntimeException(sprintf('Failed to read the file "%s"', $path));
40 | }
41 |
42 | $this->extractTranslations($catalogue, $contents);
43 | }
44 | }
45 |
46 | if ($directories) {
47 | $finder = new Finder();
48 |
49 | $finder->files()
50 | ->in($directories)
51 | ->name('*.js');
52 |
53 | foreach ($finder as $file) {
54 | $this->extractTranslations($catalogue, $file->getContents());
55 | }
56 | }
57 | }
58 |
59 | private function extractTranslations(MessageCatalogue $catalogue, string $fileContent): void
60 | {
61 | $pattern = <<set($match, $match, $this->defaultDomain);
81 | }
82 |
83 | foreach ($matches[2] as $match) {
84 | if (empty($match)) {
85 | continue;
86 | }
87 |
88 | $catalogue->set($match, $match, $this->defaultDomain);
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/DependencyInjection/IncenteevTranslationCheckerExtension.php:
--------------------------------------------------------------------------------
1 | $config
18 | */
19 | public function loadInternal(array $config, ContainerBuilder $container): void
20 | {
21 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
22 | $loader->load('services.xml');
23 |
24 | $container->registerForAutoconfiguration('Incenteev\TranslationCheckerBundle\Translator\Extractor\ExtractorInterface')
25 | ->addTag('incenteev_translation_checker.extractor');
26 |
27 | $dirs = array();
28 | $overridePathPatterns = array();
29 |
30 | if ($container->hasParameter('kernel.root_dir')) {
31 | $overridePathPatterns[] = '%%kernel.root_dir%%/Resources/%s/views';
32 | }
33 | $overridePathPatterns[] = '%%kernel.project_dir%%/templates/bundles/%s';
34 |
35 | /** @var array> $registeredBundles */
36 | $registeredBundles = $container->getParameter('kernel.bundles');
37 |
38 | foreach ($config['extraction']['bundles'] as $bundle) {
39 | if (!isset($registeredBundles[$bundle])) {
40 | throw new \InvalidArgumentException(sprintf('The bundle %s is not registered in the kernel.', $bundle));
41 | }
42 |
43 | $reflection = new \ReflectionClass($registeredBundles[$bundle]);
44 |
45 | if (false === $reflection->getFilename()) {
46 | continue;
47 | }
48 |
49 | $dirs[] = dirname($reflection->getFilename()).'/Resources/views';
50 |
51 | foreach ($overridePathPatterns as $overridePath) {
52 | $dirs[] = sprintf($overridePath, $bundle);
53 | }
54 | }
55 |
56 | if ($container->hasParameter('kernel.root_dir')) {
57 | $dirs[] = '%kernel.root_dir%/Resources/views';
58 | }
59 |
60 | $dirs[] = '%kernel.project_dir%/templates';
61 |
62 | $container->setParameter('incenteev_translation_checker.extractor.symfony.paths', $dirs);
63 |
64 | if (empty($config['extraction']['js']['paths'])) {
65 | $container->removeDefinition('incenteev_translation_checker.extractor.js');
66 | } else {
67 | $container->getDefinition('incenteev_translation_checker.extractor.js')
68 | ->replaceArgument(0, $config['extraction']['js']['paths'])
69 | ->replaceArgument(1, $config['extraction']['js']['default_domain']);
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Command/FindMissingCommand.php:
--------------------------------------------------------------------------------
1 | exposingTranslator = $exposingTranslator;
26 | $this->extractor = $extractor;
27 | }
28 |
29 | protected function configure(): void
30 | {
31 | $this->setName('incenteev:translation:find-missing')
32 | ->setDescription('Finds the missing translations in a catalogue')
33 | ->addArgument('locale', InputArgument::REQUIRED, 'The locale being checked')
34 | ->setHelp(<<%command.name% command extracts translation strings from templates
36 | of a given bundle and checks if they are available in the catalogue.
37 |
38 | php %command.full_name% en
39 |
40 | This command can only identify missing string among the string detected
41 | by the translation extractor.
42 | HELP
43 | );
44 | }
45 |
46 | protected function execute(InputInterface $input, OutputInterface $output): int
47 | {
48 | $extractedCatalogue = new MessageCatalogue($input->getArgument('locale'));
49 | $this->extractor->extract($extractedCatalogue);
50 |
51 | $loadedCatalogue = $this->exposingTranslator->getCatalogue($input->getArgument('locale'));
52 |
53 | $operation = new TargetOperation($loadedCatalogue, $extractedCatalogue);
54 |
55 | $valid = true;
56 |
57 | foreach ($operation->getDomains() as $domain) {
58 | $messages = $operation->getNewMessages($domain);
59 |
60 | if (empty($messages)) {
61 | continue;
62 | }
63 |
64 | $valid = false;
65 | $output->writeln(sprintf('%s messages are missing in the %s domain', count($messages), $domain));
66 |
67 | if ($output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
68 | continue;
69 | }
70 |
71 | foreach ($messages as $key => $translation) {
72 | $output->writeln(' '.$key);
73 | }
74 | $output->writeln('');
75 | }
76 |
77 | if ($valid) {
78 | $output->writeln(sprintf(
79 | 'The %s catalogue is in sync with the extracted one.',
80 | $input->getArgument('locale')
81 | ));
82 |
83 | return 0;
84 | }
85 |
86 | return 1;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Translation Checker Bundle
2 |
3 | This bundle provides you a few CLI commands to check your translations.
4 | These commands are designed to be usable easily in CI jobs
5 |
6 | [](https://github.com/Incenteev/translation-checker-bundle/actions/workflows/ci.yml)
7 | [](https://packagist.org/packages/incenteev/translation-checker-bundle)
8 | [](https://packagist.org/packages/incenteev/translation-checker-bundle)
9 | [](https://scrutinizer-ci.com/g/Incenteev/translation-checker-bundle/?branch=master)
10 |
11 | ## Installation
12 |
13 | Installation is a quick (I promise!) 2 step process:
14 |
15 | 1. Download IncenteevTranslationCheckerBundle
16 | 2. Enable the Bundle
17 |
18 | ### Step 1: Install IncenteevTranslationCheckerBundle with composer
19 |
20 | Run the following composer require command:
21 |
22 | ```bash
23 | composer require incenteev/translation-checker-bundle
24 | ```
25 |
26 | ### Step 2: Enable the bundle
27 |
28 | > **Note:** If you use Flex, you have nothing to do at this step, as Flex does it for you.
29 |
30 | Finally, enable the bundle in the kernel:
31 |
32 | ```php
33 | // app/AppKernel.php
34 |
35 | public function registerBundles()
36 | {
37 | $bundles = array(
38 | // ...
39 | new Incenteev\TranslationCheckerBundle\IncenteevTranslationCheckerBundle(),
40 | );
41 | }
42 | ```
43 |
44 | > **Warning:** This bundle requires that the translator is enabled in FrameworkBundle.
45 |
46 | ## Usage
47 |
48 | The bundle provides a few CLI commands. To list them all, run:
49 |
50 | ```bash
51 | bin/console list incenteev:translation
52 | ```
53 |
54 | All commands display a summary only by default. Run then in verbose mode
55 | to get a detailed report.
56 |
57 | ### Finding missing translations
58 |
59 | The `incenteev:translation:find-missing` command extracts necessary translations
60 | from our app source code, and then compare this list to the translation available
61 | for the tested locale. It will exit with a failure exit code if any missing
62 | translation is detected.
63 |
64 | > **Warning:** Translation extraction will not find all translations used by our app.
65 | > So while a failure exit code means there is an issue, a success exit code does not
66 | > guarantee that all translations are available.
67 | > The recommended usage is to use this command for your reference locale only, and
68 | > then test other locales by comparing them against the reference instead.
69 |
70 | ### Comparing translations to a reference locale
71 |
72 | The `incenteev:translation:compare` command compares available translations from
73 | 2 different locales and will exit with a failure exit code if catalogues are not
74 | in sync.
75 |
76 | > Note: this command may not work well for country variants of a locale (`fr_FR`).
77 | > Use it for main locales.
78 |
79 | ## Configuration
80 |
81 | To use the commands comparing the catalogue to the extracted translations, you
82 | need to configure the bundles in which the templates should be parsed for translations.
83 | By default, only templates in `templates` (and `app/Resources/views` on Symfony 4 and older)
84 | are registered in the extractor. You can register bundles that will be processed too.
85 |
86 | ```yaml
87 | # app/config/config.yml
88 | incenteev_translation_checker:
89 | extraction:
90 | bundles:
91 | - TwigBundle
92 | - AcmeDemoBundle
93 | ```
94 |
95 | The bundle also supports extracting translations from JS files, for projects using
96 | [willdurand/js-translation-bundle](https://packagist.org/packages/willdurand/js-translation-bundle):
97 |
98 | ```yaml
99 | # app/config/config.yml
100 | incenteev_translation_checker:
101 | extraction:
102 | js:
103 | # Paths in which JS files should be checked for translations.
104 | # Path could be either for files, or for directories in which JS files should be looked for.
105 | # This configuration is required to enable this feature.
106 | paths:
107 | - '%kernel.project_dir%/web/js'
108 | - '%kernel.project_dir%/web/other.js'
109 | # The default domain used in your JS translations. Should match the js-translation-bundle configuration
110 | # Defaults to 'messages'
111 | default_domain: js
112 | ```
113 |
--------------------------------------------------------------------------------
/src/Command/CompareCommand.php:
--------------------------------------------------------------------------------
1 | exposingTranslator = $exposingTranslator;
27 | }
28 |
29 | protected function configure(): void
30 | {
31 | $this->setName('incenteev:translation:compare')
32 | ->setDescription('Compares two translation catalogues to ensure they are in sync')
33 | ->addArgument('locale', InputArgument::REQUIRED, 'The locale being checked')
34 | ->addArgument('source', InputArgument::OPTIONAL, 'The source of the comparison', 'en')
35 | ->addOption('obsolete-only', null, InputOption::VALUE_NONE, 'Report only obsolete keys')
36 | ->addOption('domain', 'd', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'The domains being compared')
37 | ->addOption('whitelist-file', 'w', InputOption::VALUE_REQUIRED, 'Path to a YAML whitelist file')
38 | ->setHelp(<<%command.name% command compares 2 translation catalogues to
40 | ensure they are in sync. If there is missing keys or obsolete keys in the target
41 | catalogue, the command will exit with an error code.
42 |
43 | When running the command in verbose mode, the translation keys will also be displayed.
44 | php %command.full_name% fr --verbose
45 |
46 | The --domain option allows to restrict the domains being checked.
47 | It can be specified several times to check several domains. If the option is not passed,
48 | all domains will be compared.
49 |
50 | The --obsolete-only option allows to check only obsolete keys, and ignore any
51 | missing keys.
52 |
53 | The --whitelist-file option allows to define a whitelist of keys which are
54 | ignored from the comparison (they are never reported as missing or as obsolete). This
55 | file must be a Yaml file where keys are domains, and values are an array of whitelisted
56 | EOF
57 | );
58 | }
59 |
60 | protected function execute(InputInterface $input, OutputInterface $output): int
61 | {
62 | $sourceCatalogue = $this->exposingTranslator->getCatalogue($input->getArgument('source'));
63 | $comparedCatalogue = $this->exposingTranslator->getCatalogue($input->getArgument('locale'));
64 |
65 | // Change the locale of the catalogue as DiffOperation requires operating on a single locale
66 | $catalogue = new MessageCatalogue($sourceCatalogue->getLocale(), $comparedCatalogue->all());
67 |
68 | $operation = new TargetOperation($catalogue, $sourceCatalogue);
69 |
70 | $domains = $operation->getDomains();
71 | $restrictedDomains = $input->getOption('domain');
72 | if (!empty($restrictedDomains)) {
73 | $domains = array_intersect($domains, $restrictedDomains);
74 | $output->writeln(sprintf('Checking the domains %s', implode(', ', $domains)));
75 | }
76 |
77 | $checkMissing = !$input->getOption('obsolete-only');
78 |
79 | $whitelistFile = $input->getOption('whitelist-file');
80 | $whitelist = array();
81 |
82 | if (null !== $whitelistFile) {
83 | if (!file_exists($whitelistFile)) {
84 | $output->writeln(sprintf('The whitelist file "%s" does not exist.', $whitelistFile));
85 |
86 | return 1;
87 | }
88 |
89 | $whitelist = Yaml::parseFile($whitelistFile);
90 |
91 | if (!is_array($whitelist)) {
92 | $output->writeln(sprintf('The whitelist file "%s" is invalid. It must be a Yaml file containing a map.', $whitelistFile));
93 |
94 | return 1;
95 | }
96 | /** @var array $whitelist */
97 | }
98 |
99 | $valid = true;
100 |
101 | foreach ($domains as $domain) {
102 | $missingMessages = $checkMissing ? $operation->getNewMessages($domain) : array();
103 | $obsoleteMessages = $operation->getObsoleteMessages($domain);
104 | $written = false;
105 |
106 | if (isset($whitelist[$domain])) {
107 | $domainWhitelist = array_flip($whitelist[$domain]);
108 | $missingMessages = array_diff_key($missingMessages, $domainWhitelist);
109 | $obsoleteMessages = array_diff_key($obsoleteMessages, $domainWhitelist);
110 | }
111 |
112 | if (!empty($missingMessages)) {
113 | $valid = false;
114 | $written = true;
115 | $output->writeln(sprintf('%s messages are missing in the %s domain', count($missingMessages), $domain));
116 |
117 | $this->displayMessages($output, $missingMessages);
118 | }
119 |
120 | if (!empty($obsoleteMessages)) {
121 | $valid = false;
122 | $written = true;
123 | $output->writeln(sprintf('%s messages are obsolete in the %s domain', count($obsoleteMessages), $domain));
124 |
125 | $this->displayMessages($output, $obsoleteMessages);
126 | }
127 |
128 | if ($written) {
129 | $output->writeln('');
130 | }
131 | }
132 |
133 | if ($valid) {
134 | $output->writeln(sprintf(
135 | 'The %s catalogue is in sync with the %s one.',
136 | $input->getArgument('locale'),
137 | $input->getArgument('source')
138 | ));
139 |
140 | return 0;
141 | }
142 |
143 | return 1;
144 | }
145 |
146 | /**
147 | * @param array $messages
148 | */
149 | private function displayMessages(OutputInterface $output, array $messages): void
150 | {
151 | if ($output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
152 | return;
153 | }
154 |
155 | foreach ($messages as $key => $translation) {
156 | $output->writeln(' '.$key);
157 | }
158 | $output->writeln('');
159 | }
160 | }
161 |
--------------------------------------------------------------------------------