├── Changelog.md ├── LICENSE ├── README.md ├── composer.json ├── phpstan.neon └── src ├── Command ├── CompareCommand.php └── FindMissingCommand.php ├── DependencyInjection ├── Compiler │ ├── ConfigureTranslatorPass.php │ └── ExtractorPass.php ├── Configuration.php └── IncenteevTranslationCheckerExtension.php ├── IncenteevTranslationCheckerBundle.php ├── Resources └── config │ └── services.xml └── Translator └── Extractor ├── ChainExtractor.php ├── ExtractorInterface.php ├── JsExtractor.php └── SymfonyExtractor.php /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | [![CI](https://github.com/Incenteev/translation-checker-bundle/actions/workflows/ci.yml/badge.svg)](https://github.com/Incenteev/translation-checker-bundle/actions/workflows/ci.yml) 7 | [![Latest Stable Version](https://poser.pugx.org/incenteev/translation-checker-bundle/v/stable.svg)](https://packagist.org/packages/incenteev/translation-checker-bundle) 8 | [![Latest Unstable Version](https://poser.pugx.org/incenteev/translation-checker-bundle/v/unstable.svg)](https://packagist.org/packages/incenteev/translation-checker-bundle) 9 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Incenteev/translation-checker-bundle/badges/quality-score.png?b=master)](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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/IncenteevTranslationCheckerBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new ConfigureTranslatorPass()); 18 | $container->addCompilerPass(new ExtractorPass()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | %incenteev_translation_checker.extractor.symfony.paths% 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Translator/Extractor/ExtractorInterface.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/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 | --------------------------------------------------------------------------------