Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <fabien@symfony.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Symfony\Component\Translation\Command;
13
 
14
use Symfony\Component\Console\Attribute\AsCommand;
15
use Symfony\Component\Console\CI\GithubActionReporter;
16
use Symfony\Component\Console\Command\Command;
17
use Symfony\Component\Console\Completion\CompletionInput;
18
use Symfony\Component\Console\Completion\CompletionSuggestions;
19
use Symfony\Component\Console\Exception\RuntimeException;
20
use Symfony\Component\Console\Input\InputArgument;
21
use Symfony\Component\Console\Input\InputInterface;
22
use Symfony\Component\Console\Input\InputOption;
23
use Symfony\Component\Console\Output\OutputInterface;
24
use Symfony\Component\Console\Style\SymfonyStyle;
25
use Symfony\Component\Translation\Exception\InvalidArgumentException;
26
use Symfony\Component\Translation\Util\XliffUtils;
27
 
28
/**
29
 * Validates XLIFF files syntax and outputs encountered errors.
30
 *
31
 * @author Grégoire Pineau <lyrixx@lyrixx.info>
32
 * @author Robin Chalas <robin.chalas@gmail.com>
33
 * @author Javier Eguiluz <javier.eguiluz@gmail.com>
34
 */
35
#[AsCommand(name: 'lint:xliff', description: 'Lint an XLIFF file and outputs encountered errors')]
36
class XliffLintCommand extends Command
37
{
38
    private string $format;
39
    private bool $displayCorrectFiles;
40
    private ?\Closure $directoryIteratorProvider;
41
    private ?\Closure $isReadableProvider;
42
    private bool $requireStrictFileNames;
43
 
44
    public function __construct(string $name = null, callable $directoryIteratorProvider = null, callable $isReadableProvider = null, bool $requireStrictFileNames = true)
45
    {
46
        parent::__construct($name);
47
 
48
        $this->directoryIteratorProvider = null === $directoryIteratorProvider ? null : $directoryIteratorProvider(...);
49
        $this->isReadableProvider = null === $isReadableProvider ? null : $isReadableProvider(...);
50
        $this->requireStrictFileNames = $requireStrictFileNames;
51
    }
52
 
53
    protected function configure()
54
    {
55
        $this
56
            ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN')
57
            ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format')
58
            ->setHelp(<<<EOF
59
The <info>%command.name%</info> command lints an XLIFF file and outputs to STDOUT
60
the first encountered syntax error.
61
 
62
You can validates XLIFF contents passed from STDIN:
63
 
64
  <info>cat filename | php %command.full_name% -</info>
65
 
66
You can also validate the syntax of a file:
67
 
68
  <info>php %command.full_name% filename</info>
69
 
70
Or of a whole directory:
71
 
72
  <info>php %command.full_name% dirname</info>
73
  <info>php %command.full_name% dirname --format=json</info>
74
 
75
EOF
76
            )
77
        ;
78
    }
79
 
80
    protected function execute(InputInterface $input, OutputInterface $output): int
81
    {
82
        $io = new SymfonyStyle($input, $output);
83
        $filenames = (array) $input->getArgument('filename');
84
        $this->format = $input->getOption('format') ?? (GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt');
85
        $this->displayCorrectFiles = $output->isVerbose();
86
 
87
        if (['-'] === $filenames) {
88
            return $this->display($io, [$this->validate(file_get_contents('php://stdin'))]);
89
        }
90
 
91
        if (!$filenames) {
92
            throw new RuntimeException('Please provide a filename or pipe file content to STDIN.');
93
        }
94
 
95
        $filesInfo = [];
96
        foreach ($filenames as $filename) {
97
            if (!$this->isReadable($filename)) {
98
                throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename));
99
            }
100
 
101
            foreach ($this->getFiles($filename) as $file) {
102
                $filesInfo[] = $this->validate(file_get_contents($file), $file);
103
            }
104
        }
105
 
106
        return $this->display($io, $filesInfo);
107
    }
108
 
109
    private function validate(string $content, string $file = null): array
110
    {
111
        $errors = [];
112
 
113
        // Avoid: Warning DOMDocument::loadXML(): Empty string supplied as input
114
        if ('' === trim($content)) {
115
            return ['file' => $file, 'valid' => true];
116
        }
117
 
118
        $internal = libxml_use_internal_errors(true);
119
 
120
        $document = new \DOMDocument();
121
        $document->loadXML($content);
122
 
123
        if (null !== $targetLanguage = $this->getTargetLanguageFromFile($document)) {
124
            $normalizedLocalePattern = sprintf('(%s|%s)', preg_quote($targetLanguage, '/'), preg_quote(str_replace('-', '_', $targetLanguage), '/'));
125
            // strict file names require translation files to be named '____.locale.xlf'
126
            // otherwise, both '____.locale.xlf' and 'locale.____.xlf' are allowed
127
            // also, the regexp matching must be case-insensitive, as defined for 'target-language' values
128
            // http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html#target-language
129
            $expectedFilenamePattern = $this->requireStrictFileNames ? sprintf('/^.*\.(?i:%s)\.(?:xlf|xliff)/', $normalizedLocalePattern) : sprintf('/^(?:.*\.(?i:%s)|(?i:%s)\..*)\.(?:xlf|xliff)/', $normalizedLocalePattern, $normalizedLocalePattern);
130
 
131
            if (0 === preg_match($expectedFilenamePattern, basename($file))) {
132
                $errors[] = [
133
                    'line' => -1,
134
                    'column' => -1,
135
                    'message' => sprintf('There is a mismatch between the language included in the file name ("%s") and the "%s" value used in the "target-language" attribute of the file.', basename($file), $targetLanguage),
136
                ];
137
            }
138
        }
139
 
140
        foreach (XliffUtils::validateSchema($document) as $xmlError) {
141
            $errors[] = [
142
                'line' => $xmlError['line'],
143
                'column' => $xmlError['column'],
144
                'message' => $xmlError['message'],
145
            ];
146
        }
147
 
148
        libxml_clear_errors();
149
        libxml_use_internal_errors($internal);
150
 
151
        return ['file' => $file, 'valid' => 0 === \count($errors), 'messages' => $errors];
152
    }
153
 
154
    private function display(SymfonyStyle $io, array $files)
155
    {
156
        return match ($this->format) {
157
            'txt' => $this->displayTxt($io, $files),
158
            'json' => $this->displayJson($io, $files),
159
            'github' => $this->displayTxt($io, $files, true),
160
            default => throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $this->format)),
161
        };
162
    }
163
 
164
    private function displayTxt(SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false)
165
    {
166
        $countFiles = \count($filesInfo);
167
        $erroredFiles = 0;
168
        $githubReporter = $errorAsGithubAnnotations ? new GithubActionReporter($io) : null;
169
 
170
        foreach ($filesInfo as $info) {
171
            if ($info['valid'] && $this->displayCorrectFiles) {
172
                $io->comment('<info>OK</info>'.($info['file'] ? sprintf(' in %s', $info['file']) : ''));
173
            } elseif (!$info['valid']) {
174
                ++$erroredFiles;
175
                $io->text('<error> ERROR </error>'.($info['file'] ? sprintf(' in %s', $info['file']) : ''));
176
                $io->listing(array_map(function ($error) use ($info, $githubReporter) {
177
                    // general document errors have a '-1' line number
178
                    $line = -1 === $error['line'] ? null : $error['line'];
179
 
180
                    $githubReporter?->error($error['message'], $info['file'], $line, null !== $line ? $error['column'] : null);
181
 
182
                    return null === $line ? $error['message'] : sprintf('Line %d, Column %d: %s', $line, $error['column'], $error['message']);
183
                }, $info['messages']));
184
            }
185
        }
186
 
187
        if (0 === $erroredFiles) {
188
            $io->success(sprintf('All %d XLIFF files contain valid syntax.', $countFiles));
189
        } else {
190
            $io->warning(sprintf('%d XLIFF files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles));
191
        }
192
 
193
        return min($erroredFiles, 1);
194
    }
195
 
196
    private function displayJson(SymfonyStyle $io, array $filesInfo)
197
    {
198
        $errors = 0;
199
 
200
        array_walk($filesInfo, function (&$v) use (&$errors) {
201
            $v['file'] = (string) $v['file'];
202
            if (!$v['valid']) {
203
                ++$errors;
204
            }
205
        });
206
 
207
        $io->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));
208
 
209
        return min($errors, 1);
210
    }
211
 
212
    private function getFiles(string $fileOrDirectory)
213
    {
214
        if (is_file($fileOrDirectory)) {
215
            yield new \SplFileInfo($fileOrDirectory);
216
 
217
            return;
218
        }
219
 
220
        foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) {
221
            if (!\in_array($file->getExtension(), ['xlf', 'xliff'])) {
222
                continue;
223
            }
224
 
225
            yield $file;
226
        }
227
    }
228
 
229
    private function getDirectoryIterator(string $directory)
230
    {
231
        $default = function ($directory) {
232
            return new \RecursiveIteratorIterator(
233
                new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
234
                \RecursiveIteratorIterator::LEAVES_ONLY
235
            );
236
        };
237
 
238
        if (null !== $this->directoryIteratorProvider) {
239
            return ($this->directoryIteratorProvider)($directory, $default);
240
        }
241
 
242
        return $default($directory);
243
    }
244
 
245
    private function isReadable(string $fileOrDirectory)
246
    {
247
        $default = function ($fileOrDirectory) {
248
            return is_readable($fileOrDirectory);
249
        };
250
 
251
        if (null !== $this->isReadableProvider) {
252
            return ($this->isReadableProvider)($fileOrDirectory, $default);
253
        }
254
 
255
        return $default($fileOrDirectory);
256
    }
257
 
258
    private function getTargetLanguageFromFile(\DOMDocument $xliffContents): ?string
259
    {
260
        foreach ($xliffContents->getElementsByTagName('file')[0]->attributes ?? [] as $attribute) {
261
            if ('target-language' === $attribute->nodeName) {
262
                return $attribute->nodeValue;
263
            }
264
        }
265
 
266
        return null;
267
    }
268
 
269
    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
270
    {
271
        if ($input->mustSuggestOptionValuesFor('format')) {
272
            $suggestions->suggestValues(['txt', 'json', 'github']);
273
        }
274
    }
275
}