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

move-elevator / composer-translation-validator / 16518498621

25 Jul 2025 09:18AM UTC coverage: 96.457% (+0.001%) from 96.456%
16518498621

Pull #46

github

jackd248
Merge remote-tracking branch 'origin/main' into key-naming-validator

# Conflicts:
#	README.md
#	src/Command/ValidateTranslationCommand.php
#	src/Validator/ValidatorRegistry.php
#	tests/src/Validator/ValidatorRegistryTest.php
Pull Request #46: feat: add KeyNamingConventionValidator with configurable naming conventions

139 of 144 new or added lines in 6 files covered. (96.53%)

22 existing lines in 6 files now uncovered.

2042 of 2117 relevant lines covered (96.46%)

8.42 hits per line

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

97.71
/src/Validator/MismatchValidator.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the Composer plugin "composer-translation-validator".
7
 *
8
 * Copyright (C) 2025 Konrad Michalik <km@move-elevator.de>
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 */
23

24
namespace MoveElevator\ComposerTranslationValidator\Validator;
25

26
use MoveElevator\ComposerTranslationValidator\FileDetector\FileSet;
27
use MoveElevator\ComposerTranslationValidator\Parser\JsonParser;
28
use MoveElevator\ComposerTranslationValidator\Parser\ParserInterface;
29
use MoveElevator\ComposerTranslationValidator\Parser\PhpParser;
30
use MoveElevator\ComposerTranslationValidator\Parser\XliffParser;
31
use MoveElevator\ComposerTranslationValidator\Parser\YamlParser;
32
use MoveElevator\ComposerTranslationValidator\Result\Issue;
33
use Symfony\Component\Console\Helper\Table;
34
use Symfony\Component\Console\Helper\TableStyle;
35
use Symfony\Component\Console\Output\OutputInterface;
36

37
class MismatchValidator extends AbstractValidator implements ValidatorInterface
38
{
39
    /** @var array<string, array<string>> */
40
    /**
41
     * @var array<string, array<string, string|null>>
42
     */
43
    protected array $keyArray = [];
44

45
    public function processFile(ParserInterface $file): array
4✔
46
    {
47
        $keys = $file->extractKeys();
4✔
48

49
        if (!$keys) {
4✔
50
            $this->logger?->error('The source file '.$file->getFileName().' is not valid.');
1✔
51

52
            return [];
1✔
53
        }
54
        foreach ($keys as $key) {
3✔
55
            $value = $file->getContentByKey($key);
3✔
56
            $this->keyArray[$file->getFileName()][$key] = $value ?? null;
3✔
57
        }
58

59
        return [];
3✔
60
    }
61

62
    public function postProcess(): void
2✔
63
    {
64
        $allKeys = [];
2✔
65
        foreach ($this->keyArray as $values) {
2✔
66
            $allKeys[] = array_keys($values);
2✔
67
        }
68
        $allKeys = array_unique(array_merge(...$allKeys));
2✔
69

70
        foreach ($allKeys as $key) {
2✔
71
            $missingInSome = false;
2✔
72
            foreach ($this->keyArray as $keys) {
2✔
73
                if (!array_key_exists($key, $keys)) {
2✔
74
                    $missingInSome = true;
1✔
75
                    break;
1✔
76
                }
77
            }
78
            if ($missingInSome) {
2✔
79
                $result = [
1✔
80
                    'key' => $key,
1✔
81
                    'files' => [],
1✔
82
                ];
1✔
83
                foreach ($this->keyArray as $file => $keys) {
1✔
84
                    $result['files'][] = [
1✔
85
                        'file' => $file,
1✔
86
                        'value' => $keys[$key] ?? null,
1✔
87
                    ];
1✔
88
                }
89
                $this->addIssue(new Issue(
1✔
90
                    '',
1✔
91
                    $result,
1✔
92
                    '',
1✔
93
                    $this->getShortName(),
1✔
94
                ));
1✔
95
            }
96
        }
97
    }
98

99
    public function formatIssueMessage(Issue $issue, string $prefix = ''): string
1✔
100
    {
101
        $details = $issue->getDetails();
1✔
102
        $resultType = $this->resultTypeOnValidationFailure();
1✔
103

104
        $level = $resultType->toString();
1✔
105
        $color = $resultType->toColorString();
1✔
106

107
        $key = $details['key'] ?? 'unknown';
1✔
108
        $files = $details['files'] ?? [];
1✔
109
        $currentFile = basename($issue->getFile());
1✔
110
        $otherFiles = [];
1✔
111
        $currentFileHasValue = false;
1✔
112

113
        foreach ($files as $fileInfo) {
1✔
114
            $fileName = $fileInfo['file'] ?? 'unknown';
1✔
115
            if ($fileName === $currentFile) {
1✔
UNCOV
116
                $currentFileHasValue = null !== $fileInfo['value'];
×
117
            } else {
118
                $otherFiles[] = $fileName;
1✔
119
            }
120
        }
121

122
        if ($currentFileHasValue) {
1✔
UNCOV
123
            $action = 'missing from';
×
124
        } else {
125
            $action = 'missing but present in';
1✔
126
        }
127

128
        $otherFilesList = !empty($otherFiles) ? implode('`, `', $otherFiles) : 'other files';
1✔
129

130
        return "- <fg=$color>$level</> {$prefix} the translation key `$key` is $action other translation files (`$otherFilesList`)";
1✔
131
    }
132

133
    public function distributeIssuesForDisplay(FileSet $fileSet): array
1✔
134
    {
135
        $distribution = [];
1✔
136

137
        foreach ($this->issues as $issue) {
1✔
138
            $details = $issue->getDetails();
1✔
139
            $files = $details['files'] ?? [];
1✔
140

141
            foreach ($files as $fileInfo) {
1✔
142
                $fileName = $fileInfo['file'] ?? '';
1✔
143
                if (!empty($fileName)) {
1✔
144
                    $basePath = rtrim($fileSet->getPath(), '/');
1✔
145
                    $filePath = $basePath.'/'.$fileName;
1✔
146

147
                    $fileSpecificIssue = new Issue(
1✔
148
                        $filePath,
1✔
149
                        $details,
1✔
150
                        $issue->getParser(),
1✔
151
                        $issue->getValidatorType(),
1✔
152
                    );
1✔
153

154
                    if (!isset($distribution[$filePath])) {
1✔
155
                        $distribution[$filePath] = [];
1✔
156
                    }
157

158
                    $distribution[$filePath][] = $fileSpecificIssue;
1✔
159
                }
160
            }
161
        }
162

163
        return $distribution;
1✔
164
    }
165

166
    public function renderDetailedOutput(OutputInterface $output, array $issues): void
1✔
167
    {
168
        if (empty($issues)) {
1✔
UNCOV
169
            return;
×
170
        }
171

172
        $rows = [];
1✔
173
        $allKeys = [];
1✔
174
        $allFilesData = [];
1✔
175

176
        foreach ($issues as $issue) {
1✔
177
            $details = $issue->getDetails();
1✔
178
            $key = $details['key'] ?? 'unknown';
1✔
179
            $files = $details['files'] ?? [];
1✔
180
            $currentFile = basename($issue->getFile());
1✔
181

182
            if (!in_array($key, $allKeys)) {
1✔
183
                $allKeys[] = $key;
1✔
184
            }
185

186
            foreach ($files as $fileInfo) {
1✔
187
                $fileName = $fileInfo['file'] ?? '';
1✔
188
                $value = $fileInfo['value'];
1✔
189

190
                if (!isset($allFilesData[$key])) {
1✔
191
                    $allFilesData[$key] = [];
1✔
192
                }
193
                $allFilesData[$key][$fileName] = $value;
1✔
194
            }
195
        }
196

197
        $firstIssue = $issues[0];
1✔
198
        $currentFile = basename($firstIssue->getFile());
1✔
199
        $firstDetails = $firstIssue->getDetails();
1✔
200
        $firstFiles = $firstDetails['files'] ?? [];
1✔
201

202
        $fileOrder = [$currentFile];
1✔
203
        foreach ($firstFiles as $fileInfo) {
1✔
204
            $fileName = $fileInfo['file'] ?? '';
1✔
205
            if ($fileName !== $currentFile && !in_array($fileName, $fileOrder, true)) {
1✔
206
                $fileOrder[] = $fileName;
1✔
207
            }
208
        }
209

210
        $header = ['Translation Key', $currentFile];
1✔
211
        foreach ($fileOrder as $fileName) {
1✔
212
            if ($fileName !== $currentFile) {
1✔
213
                $header[] = $fileName;
1✔
214
            }
215
        }
216

217
        foreach ($allKeys as $key) {
1✔
218
            $row = [$key];
1✔
219
            foreach ($fileOrder as $fileName) {
1✔
220
                $value = $allFilesData[$key][$fileName] ?? null;
1✔
221
                $row[] = $value ?? '';
1✔
222
            }
223
            $rows[] = $row;
1✔
224
        }
225

226
        $table = new Table($output);
1✔
227
        $table->setHeaders($header)
1✔
228
            ->setRows($rows)
1✔
229
            ->setStyle(
1✔
230
                (new TableStyle())
1✔
231
                    ->setCellHeaderFormat('%s'),
1✔
232
            )
1✔
233
            ->render();
1✔
234
    }
235

236
    /**
237
     * @return class-string<ParserInterface>[]
238
     */
239
    public function supportsParser(): array
1✔
240
    {
241
        return [XliffParser::class, YamlParser::class, JsonParser::class, PhpParser::class];
1✔
242
    }
243

244
    protected function resetState(): void
1✔
245
    {
246
        parent::resetState();
1✔
247
        $this->keyArray = [];
1✔
248
    }
249

250
    public function resultTypeOnValidationFailure(): ResultType
1✔
251
    {
252
        return ResultType::WARNING;
1✔
253
    }
254

255
    public function shouldShowDetailedOutput(): bool
1✔
256
    {
257
        return true;
1✔
258
    }
259
}
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