• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

move-elevator / composer-translation-validator / 28712492500

04 Jul 2026 04:29PM UTC coverage: 99.92% (-0.08%) from 100.0%
28712492500

Pull #139

github

konradmichalik
fix: reject invalid validator classes in --only/--skip options
Pull Request #139: fix: reject invalid validator classes in --only/--skip options

12 of 14 new or added lines in 2 files covered. (85.71%)

2494 of 2496 relevant lines covered (99.92%)

9.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.39
/src/Command/ValidateTranslationCommand.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the "composer-translation-validator" Composer plugin.
7
 *
8
 * (c) 2025-2026 Konrad Michalik <km@move-elevator.de>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13

14
namespace MoveElevator\ComposerTranslationValidator\Command;
15

16
use Composer\Command\BaseCommand;
17
use JsonException;
18
use MoveElevator\ComposerTranslationValidator\Config\CommandConfigResolver;
19
use MoveElevator\ComposerTranslationValidator\Result\{FormatType, Output};
20
use MoveElevator\ComposerTranslationValidator\Service\ValidationOrchestrationService;
21
use MoveElevator\ComposerTranslationValidator\Validator\ValidatorInterface;
22
use Psr\Log\LoggerInterface;
23
use ReflectionException;
24
use RuntimeException;
25
use Symfony\Component\Console\Command\Command;
26
use Symfony\Component\Console\Input\{InputArgument, InputInterface, InputOption};
27
use Symfony\Component\Console\Logger\ConsoleLogger;
28
use Symfony\Component\Console\Output\OutputInterface;
29
use Symfony\Component\Console\Style\SymfonyStyle;
30

31
/**
32
 * ValidateTranslationCommand.
33
 *
34
 * @author Konrad Michalik <km@move-elevator.de>
35
 * @license GPL-3.0-or-later
36
 */
37
class ValidateTranslationCommand extends BaseCommand
38
{
39
    protected ?SymfonyStyle $io = null;
40
    protected ?InputInterface $input = null;
41
    protected ?OutputInterface $output = null;
42

43
    protected LoggerInterface $logger;
44
    protected ValidationOrchestrationService $orchestrationService;
45
    protected CommandConfigResolver $configResolver;
46

47
    protected bool $dryRun = false;
48
    protected bool $strict = false;
49

50
    protected function configure(): void
19✔
51
    {
52
        $this->setName('validate-translations')
19✔
53
            ->setAliases(['vt'])
19✔
54
            ->setDescription('Validates translation files with several validators.')
19✔
55
            ->addArgument(
19✔
56
                'path',
19✔
57
                InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
19✔
58
                'Paths to the folders containing translation files',
19✔
59
            )
19✔
60
            ->addOption(
19✔
61
                'dry-run',
19✔
62
                null,
19✔
63
                InputOption::VALUE_NONE,
19✔
64
                'Run the command in dry-run mode without throwing errors',
19✔
65
            )
19✔
66
            ->addOption(
19✔
67
                'strict',
19✔
68
                null,
19✔
69
                InputOption::VALUE_NONE,
19✔
70
                'Fail on warnings as errors',
19✔
71
            )
19✔
72
            ->addOption(
19✔
73
                'format',
19✔
74
                'f',
19✔
75
                InputOption::VALUE_OPTIONAL,
19✔
76
                'Output format: cli, json, or github',
19✔
77
                FormatType::CLI->value,
19✔
78
            )
19✔
79
            ->addOption(
19✔
80
                'only',
19✔
81
                'o',
19✔
82
                InputOption::VALUE_OPTIONAL,
19✔
83
                'The specific validators to use (FQCN), comma-separated',
19✔
84
            )
19✔
85
            ->addOption(
19✔
86
                'skip',
19✔
87
                's',
19✔
88
                InputOption::VALUE_OPTIONAL,
19✔
89
                'Skip specific validators (FQCN), comma-separated',
19✔
90
            )
19✔
91
            ->addOption(
19✔
92
                'config',
19✔
93
                'c',
19✔
94
                InputOption::VALUE_OPTIONAL,
19✔
95
                'Path to the configuration file',
19✔
96
            )
19✔
97
            ->addOption(
19✔
98
                'recursive',
19✔
99
                'r',
19✔
100
                InputOption::VALUE_NONE,
19✔
101
                'Search for translation files recursively in subdirectories',
19✔
102
            )
19✔
103
            ->addOption(
19✔
104
                'exclude',
19✔
105
                'e',
19✔
106
                InputOption::VALUE_OPTIONAL,
19✔
107
                'Exclude files matching glob patterns, comma-separated (e.g., "**/backup/**,**/*.bak")',
19✔
108
            )
19✔
109
            ->setHelp(
19✔
110
                <<<HELP
19✔
111
The <info>validate-translations</info> command validates translation files (XLIFF, YAML, JSON and PHP)
112
using multiple validators to ensure consistency, correctness and schema compliance.
113

114
<comment>Usage:</comment>
115
  <info>composer validate-translations <path> [options]</info>
116

117
<comment>Examples:</comment>
118
  <info>composer validate-translations translations/</info>
119
  <info>composer validate-translations translations/ --recursive</info>
120
  <info>composer validate-translations translations/ -r --format json</info>
121
  <info>composer validate-translations translations/ --format github</info>
122
  <info>composer validate-translations translations/ --dry-run</info>
123
  <info>composer validate-translations translations/ --strict</info>
124
  <info>composer validate-translations translations/ --exclude "**/backup/**,**/*.bak"</info>
125
  <info>composer validate-translations translations/ --only \</info>
126
    <info>"MoveElevator\ComposerTranslationValidator\Validator\DuplicateKeysValidator"</info>
127

128
<comment>Available Validators:</comment>
129
  • <info>MismatchValidator</info>        - Detects mismatches between source and target
130
  • <info>DuplicateKeysValidator</info>   - Finds duplicate translation keys
131
  • <info>DuplicateValuesValidator</info> - Finds duplicate translation values (opt-in, disabled by default)
132
  • <info>EmptyValuesValidator</info>     - Finds empty or whitespace-only translation values
133
  • <info>EncodingValidator</info>        - Validates file encoding and character issues
134
  • <info>HtmlTagValidator</info>         - Validates HTML tag consistency across translations
135
  • <info>KeyNamingConventionValidator</info> - Validates translation key naming conventions
136
  • <info>KeyCountValidator</info>        - Warns when a file exceeds the key count threshold
137
  • <info>KeyDepthValidator</info>        - Warns when keys exceed the nesting depth threshold
138
  • <info>PlaceholderConsistencyValidator</info> - Validates placeholder consistency across files
139
  • <info>XliffSchemaValidator</info>     - Validates XLIFF schema compliance
140

141
<comment>Configuration:</comment>
142
You can configure the validator using:
143
  1. Command line options
144
  2. A configuration file (--config option)
145
  3. Settings in composer.json under "extra.translation-validator"
146
  4. Auto-detection from project structure
147

148
<comment>Output Formats:</comment>
149
  • <info>cli</info>    - Human-readable console output (default)
150
  • <info>json</info>   - Machine-readable JSON output
151
  • <info>github</info> - GitHub Actions workflow commands for CI integration
152

153
<comment>Modes:</comment>
154
  • <info>--dry-run</info> - Run validation without failing on errors
155
  • <info>--strict</info>  - Treat warnings as errors
156
HELP
19✔
157
            );
19✔
158
    }
159

160
    protected function initialize(InputInterface $input, OutputInterface $output): void
18✔
161
    {
162
        $this->input = $input;
18✔
163
        $this->output = $output;
18✔
164
        $this->io = new SymfonyStyle($input, $output);
18✔
165
        $this->logger = new ConsoleLogger($output);
18✔
166
        $this->orchestrationService = new ValidationOrchestrationService($this->logger);
18✔
167
        $this->configResolver = new CommandConfigResolver();
18✔
168
    }
169

170
    /**
171
     * @throws ReflectionException|JsonException
172
     */
173
    protected function execute(InputInterface $input, OutputInterface $output): int
18✔
174
    {
175
        $config = $this->configResolver->resolve($input);
18✔
176

177
        $inputPaths = $input->getArgument('path') ?: [];
18✔
178
        $paths = $this->orchestrationService->resolvePaths($inputPaths, $config);
18✔
179

180
        $this->dryRun = $config->getDryRun();
18✔
181
        $this->strict = $config->getStrict();
18✔
182
        $recursive = (bool) $input->getOption('recursive');
18✔
183

184
        $excludePatterns = $config->getExclude();
18✔
185

186
        $fileDetector = $this->orchestrationService->resolveFileDetector($config);
18✔
187

188
        if (empty($paths)) {
18✔
189
            $this->io?->error('No paths provided.');
1✔
190

191
            return Command::FAILURE;
1✔
192
        }
193

194
        $onlyOption = $input->getOption('only');
17✔
195
        $skipOption = $input->getOption('skip');
17✔
196

197
        $onlyValidators = $this->orchestrationService->validateClassInput(
17✔
198
            ValidatorInterface::class,
17✔
199
            'validator',
17✔
200
            $onlyOption,
17✔
201
        );
17✔
202
        $skipValidators = $this->orchestrationService->validateClassInput(
17✔
203
            ValidatorInterface::class,
17✔
204
            'validator',
17✔
205
            $skipOption,
17✔
206
        );
17✔
207

208
        if (!empty($onlyOption) && empty($onlyValidators)) {
17✔
209
            $this->io?->error('The "--only" option contains no valid validators.');
1✔
210

211
            return Command::FAILURE;
1✔
212
        }
213

214
        if (!empty($skipOption) && empty($skipValidators)) {
16✔
NEW
215
            $this->io?->error('The "--skip" option contains no valid validators.');
×
216

NEW
217
            return Command::FAILURE;
×
218
        }
219

220
        $validators = $this->orchestrationService->resolveValidators($onlyValidators, $skipValidators, $config);
16✔
221

222
        $validationResult = $this->orchestrationService->executeValidation(
16✔
223
            $paths,
16✔
224
            $excludePatterns,
16✔
225
            $recursive,
16✔
226
            $fileDetector,
16✔
227
            $validators,
16✔
228
            $config,
16✔
229
        );
16✔
230

231
        if (null === $validationResult) {
16✔
232
            $this->io?->warning('No files found in the specified directories.');
1✔
233

234
            return Command::SUCCESS;
1✔
235
        }
236

237
        $format = FormatType::tryFrom($config->getFormat());
15✔
238

239
        if (null === $format) {
15✔
240
            $this->io?->error('Invalid output format specified. Use "cli", "json" or "github".');
1✔
241

242
            return Command::FAILURE;
1✔
243
        }
244

245
        // @codeCoverageIgnoreStart
246
        if (null === $this->output || null === $this->input) {
247
            throw new RuntimeException('Output or Input interface not initialized');
248
        }
249
        // @codeCoverageIgnoreEnd
250

251
        return (new Output(
14✔
252
            $this->logger,
14✔
253
            $this->output,
14✔
254
            $this->input,
14✔
255
            $format,
14✔
256
            $validationResult,
14✔
257
            $this->dryRun,
14✔
258
            $this->strict,
14✔
259
        ))->summarize();
14✔
260
    }
261
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc