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

move-elevator / composer-translation-validator / 25277319213

03 May 2026 10:59AM UTC coverage: 95.606% (+0.03%) from 95.573%
25277319213

push

github

web-flow
Merge pull request #119 from maikschneider/target-language-check

Validate target-language in XLF files

55 of 57 new or added lines in 2 files covered. (96.49%)

1 existing line in 1 file now uncovered.

2415 of 2526 relevant lines covered (95.61%)

8.79 hits per line

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

95.0
/src/Validator/XliffSchemaValidator.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\Validator;
15

16
use Exception;
17
use MoveElevator\ComposerTranslationValidator\Parser\{ParserInterface, XliffParser};
18
use MoveElevator\ComposerTranslationValidator\Result\Issue;
19
use Symfony\Component\Config\Util\XmlUtils;
20
use Symfony\Component\Translation\Util\XliffUtils;
21

22
use function sprintf;
23
use function strtolower;
24

25
/**
26
 * XliffSchemaValidator.
27
 *
28
 * @author Konrad Michalik <km@move-elevator.de>
29
 * @license GPL-3.0-or-later
30
 */
31
class XliffSchemaValidator extends AbstractValidator implements ValidatorInterface
32
{
33
    public function processFile(ParserInterface $file): array
13✔
34
    {
35
        /*
36
         * With XmlUtils::loadFile() we always get a strange symfony error related to global composer autoloading issue.
37
         *      Call to undefined method Symfony\Component\Filesystem\Filesystem::readFile()
38
         */
39
        if (!file_exists($file->getFilePath())) {
13✔
40
            $this->logger?->error('File does not exist: '.$file->getFileName());
2✔
41

42
            return [];
2✔
43
        }
44

45
        $fileContent = file_get_contents($file->getFilePath());
11✔
46
        if (false === $fileContent) {
11✔
NEW
47
            $this->logger?->error('Failed to read file: '.$file->getFileName());
×
48

NEW
49
            return [];
×
50
        }
51

52
        try {
53
            $dom = XmlUtils::parse($fileContent);
11✔
54
        } catch (Exception $e) {
1✔
55
            $this->logger?->error('Failed to parse XML: '.$e->getMessage());
1✔
56

57
            return [];
1✔
58
        }
59

60
        // Schema validation — may throw for unsupported XLIFF versions (e.g. 2.x)
61
        $errors = [];
10✔
62
        try {
63
            $errors = XliffUtils::validateSchema($dom);
10✔
64
        } catch (Exception $e) {
1✔
65
            if (str_contains($e->getMessage(), 'No support implemented for loading XLIFF version')) {
1✔
66
                $this->logger?->notice(sprintf('Skipping %s: %s', $this->getShortName(), $e->getMessage()));
1✔
67
            } else {
UNCOV
68
                $this->logger?->error('Failed to validate XML schema: '.$e->getMessage());
×
69
            }
70
        }
71

72
        // Additional check: if filename encodes a locale, verify it matches target-language in the file header
73
        if (!$file instanceof XliffParser) {
10✔
74
            return $errors;
2✔
75
        }
76

77
        $expectedLanguage = $file->getLanguageFromFileName();
8✔
78
        if (null !== $expectedLanguage) {
8✔
79
            $targetLang = $file->getTargetLanguage();
5✔
80
            $isVersion2 = $file->isVersion2();
5✔
81
            $attribute = $isVersion2 ? 'trgLang' : 'target-language';
5✔
82
            $element = $isVersion2 ? '<xliff>' : '<file>';
5✔
83

84
            if (null === $targetLang) {
5✔
85
                $errors[] = [
2✔
86
                    'message' => sprintf(
2✔
87
                        'Missing "%s" attribute on %s node; expected "%s" based on filename',
2✔
88
                        $attribute,
2✔
89
                        $element,
2✔
90
                        $expectedLanguage,
2✔
91
                    ),
2✔
92
                    'level' => 'ERROR',
2✔
93
                ];
2✔
94
            } elseif (strtolower($targetLang) !== $expectedLanguage) {
3✔
95
                $errors[] = [
2✔
96
                    'message' => sprintf(
2✔
97
                        '"%s" attribute "%s" does not match filename language "%s"',
2✔
98
                        $attribute,
2✔
99
                        $targetLang,
2✔
100
                        $expectedLanguage,
2✔
101
                    ),
2✔
102
                    'level' => 'ERROR',
2✔
103
                ];
2✔
104
            }
105
        }
106

107
        return $errors;
8✔
108
    }
109

110
    public function formatIssueMessage(Issue $issue, string $prefix = ''): string
8✔
111
    {
112
        $details = $issue->getDetails();
8✔
113

114
        // Since AbstractValidator creates one Issue per error array,
115
        // $details is the individual error array, not an array of errors
116
        if (isset($details['message'])) {
8✔
117
            $message = $details['message'];
5✔
118
            $line = isset($details['line']) ? " (Line: {$details['line']})" : '';
5✔
119
            $code = isset($details['code']) ? " (Code: {$details['code']})" : '';
5✔
120
            $level = $details['level'] ?? 'ERROR';
5✔
121

122
            $color = 'ERROR' === strtoupper((string) $level) ? 'red' : 'yellow';
5✔
123
            $levelText = ucfirst(strtolower((string) $level));
5✔
124

125
            return "- <fg=$color>$levelText</> {$prefix}$message$line$code";
5✔
126
        }
127

128
        return "- <fg=red>Error</> {$prefix}Schema validation error";
3✔
129
    }
130

131
    /**
132
     * @return class-string<ParserInterface>[]
133
     */
134
    public function supportsParser(): array
3✔
135
    {
136
        return [XliffParser::class];
3✔
137
    }
138
}
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