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

move-elevator / composer-translation-validator / 18559927341

16 Oct 2025 11:35AM UTC coverage: 95.519%. Remained the same
18559927341

Pull #73

github

jackd248
build: add php-cs-fixer-preset
Pull Request #73: build: add php-cs-fixer-preset

206 of 210 new or added lines in 16 files covered. (98.1%)

91 existing lines in 20 files now uncovered.

2345 of 2455 relevant lines covered (95.52%)

7.73 hits per line

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

96.77
/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 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\{ConfigReader, TranslationValidatorConfig};
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

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

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

107
<comment>Usage:</comment>
108
  <info>composer validate-translations <path> [options]</info>
109

110
<comment>Examples:</comment>
111
  <info>composer validate-translations translations/</info>
112
  <info>composer validate-translations translations/ --recursive</info>
113
  <info>composer validate-translations translations/ -r --format json</info>
114
  <info>composer validate-translations translations/ --format github</info>
115
  <info>composer validate-translations translations/ --dry-run</info>
116
  <info>composer validate-translations translations/ --strict</info>
117
  <info>composer validate-translations translations/ --only \</info>
118
    <info>"MoveElevator\ComposerTranslationValidator\Validator\DuplicateKeysValidator"</info>
119

120
<comment>Available Validators:</comment>
121
  • <info>MismatchValidator</info>        - Detects mismatches between source and target
122
  • <info>DuplicateKeysValidator</info>   - Finds duplicate translation keys
123
  • <info>DuplicateValuesValidator</info> - Finds duplicate translation values
124
  • <info>EmptyValuesValidator</info>     - Finds empty or whitespace-only translation values
125
  • <info>EncodingValidator</info>        - Validates file encoding and character issues
126
  • <info>HtmlTagValidator</info>         - Validates HTML tag consistency across translations
127
  • <info>KeyNamingConventionValidator</info> - Validates translation key naming conventions
128
  • <info>PlaceholderConsistencyValidator</info> - Validates placeholder consistency across files
129
  • <info>XliffSchemaValidator</info>     - Validates XLIFF schema compliance
130

131
<comment>Configuration:</comment>
132
You can configure the validator using:
133
  1. Command line options
134
  2. A configuration file (--config option)
135
  3. Settings in composer.json under "extra.translation-validator"
136
  4. Auto-detection from project structure
137

138
<comment>Output Formats:</comment>
139
  • <info>cli</info>    - Human-readable console output (default)
140
  • <info>json</info>   - Machine-readable JSON output
141
  • <info>github</info> - GitHub Actions workflow commands for CI integration
142

143
<comment>Modes:</comment>
144
  • <info>--dry-run</info> - Run validation without failing on errors
145
  • <info>--strict</info>  - Treat warnings as errors
146
HELP
16✔
147
            );
16✔
148
    }
149

150
    protected function initialize(InputInterface $input, OutputInterface $output): void
15✔
151
    {
152
        $this->input = $input;
15✔
153
        $this->output = $output;
15✔
154
        $this->io = new SymfonyStyle($input, $output);
15✔
155
        $this->logger = new ConsoleLogger($output);
15✔
156
        $this->orchestrationService = new ValidationOrchestrationService($this->logger);
15✔
157
    }
158

159
    /**
160
     * @throws ReflectionException|JsonException
161
     */
162
    protected function execute(InputInterface $input, OutputInterface $output): int
15✔
163
    {
164
        $config = $this->loadConfiguration($input);
15✔
165

166
        $inputPaths = $input->getArgument('path') ?: [];
15✔
167
        $paths = $this->orchestrationService->resolvePaths($inputPaths, $config);
15✔
168

169
        $this->dryRun = $config->getDryRun() || $input->getOption('dry-run');
15✔
170
        $this->strict = $config->getStrict() || $input->getOption('strict');
15✔
171
        $recursive = (bool) $input->getOption('recursive');
15✔
172
        $excludePatterns = $config->getExclude();
15✔
173

174
        $fileDetector = $this->orchestrationService->resolveFileDetector($config);
15✔
175

176
        if (empty($paths)) {
15✔
177
            $this->io?->error('No paths provided.');
1✔
178

179
            return Command::FAILURE;
1✔
180
        }
181

182
        $onlyValidators = $this->orchestrationService->validateClassInput(
14✔
183
            ValidatorInterface::class,
14✔
184
            'validator',
14✔
185
            $input->getOption('only'),
14✔
186
        );
14✔
187
        $skipValidators = $this->orchestrationService->validateClassInput(
14✔
188
            ValidatorInterface::class,
14✔
189
            'validator',
14✔
190
            $input->getOption('skip'),
14✔
191
        );
14✔
192

193
        $validators = $this->orchestrationService->resolveValidators($onlyValidators, $skipValidators, $config);
14✔
194

195
        $validationResult = $this->orchestrationService->executeValidation(
14✔
196
            $paths,
14✔
197
            $excludePatterns,
14✔
198
            $recursive,
14✔
199
            $fileDetector,
14✔
200
            $validators,
14✔
201
            $config,
14✔
202
        );
14✔
203

204
        if (null === $validationResult) {
13✔
205
            $this->io?->warning('No files found in the specified directories.');
1✔
206

207
            return Command::SUCCESS;
1✔
208
        }
209

210
        $format = FormatType::tryFrom($input->getOption('format') ?: $config->getFormat());
12✔
211

212
        if (null === $format) {
12✔
213
            $this->io?->error('Invalid output format specified. Use "cli", "json" or "github".');
1✔
214

215
            return Command::FAILURE;
1✔
216
        }
217

218
        if (null === $this->output || null === $this->input) {
11✔
UNCOV
219
            throw new RuntimeException('Output or Input interface not initialized');
×
220
        }
221

222
        return (new Output(
11✔
223
            $this->logger,
11✔
224
            $this->output,
11✔
225
            $this->input,
11✔
226
            $format,
11✔
227
            $validationResult,
11✔
228
            $this->dryRun,
11✔
229
            $this->strict,
11✔
230
        ))->summarize();
11✔
231
    }
232

233
    /**
234
     * @throws JsonException
235
     */
236
    private function loadConfiguration(InputInterface $input): TranslationValidatorConfig
15✔
237
    {
238
        $configReader = new ConfigReader();
15✔
239
        $configPath = $input->getOption('config');
15✔
240

241
        if ($configPath) {
15✔
UNCOV
242
            return $configReader->read($configPath);
×
243
        }
244

245
        // Try to load from composer.json
246
        $composerJsonPath = getcwd().'/composer.json';
15✔
247
        $config = $configReader->readFromComposerJson($composerJsonPath);
15✔
248
        if ($config) {
15✔
UNCOV
249
            return $config;
×
250
        }
251

252
        // Try auto-detection
253
        $config = $configReader->autoDetect();
15✔
254
        if ($config) {
15✔
255
            return $config;
×
256
        }
257

258
        // Return default configuration
259
        return new TranslationValidatorConfig();
15✔
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

© 2025 Coveralls, Inc