blob: 35ad33efacffbc8cac506b387b18e044c3b1faac [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?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
12namespace Symfony\Component\Translation\Loader;
13
14use Symfony\Component\Config\Resource\FileResource;
15use Symfony\Component\Config\Util\Exception\InvalidXmlException;
16use Symfony\Component\Config\Util\Exception\XmlParsingException;
17use Symfony\Component\Config\Util\XmlUtils;
18use Symfony\Component\Translation\Exception\InvalidResourceException;
19use Symfony\Component\Translation\Exception\NotFoundResourceException;
20use Symfony\Component\Translation\Exception\RuntimeException;
21use Symfony\Component\Translation\MessageCatalogue;
22use Symfony\Component\Translation\Util\XliffUtils;
23
24/**
25 * XliffFileLoader loads translations from XLIFF files.
26 *
27 * @author Fabien Potencier <fabien@symfony.com>
28 */
29class XliffFileLoader implements LoaderInterface
30{
31 /**
32 * {@inheritdoc}
33 */
34 public function load($resource, string $locale, string $domain = 'messages')
35 {
36 if (!class_exists(XmlUtils::class)) {
37 throw new RuntimeException('Loading translations from the Xliff format requires the Symfony Config component.');
38 }
39
40 if (!$this->isXmlString($resource)) {
41 if (!stream_is_local($resource)) {
42 throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
43 }
44
45 if (!file_exists($resource)) {
46 throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
47 }
48
49 if (!is_file($resource)) {
50 throw new InvalidResourceException(sprintf('This is neither a file nor an XLIFF string "%s".', $resource));
51 }
52 }
53
54 try {
55 if ($this->isXmlString($resource)) {
56 $dom = XmlUtils::parse($resource);
57 } else {
58 $dom = XmlUtils::loadFile($resource);
59 }
60 } catch (\InvalidArgumentException | XmlParsingException | InvalidXmlException $e) {
61 throw new InvalidResourceException(sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $e);
62 }
63
64 if ($errors = XliffUtils::validateSchema($dom)) {
65 throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: ', $resource).XliffUtils::getErrorsAsString($errors));
66 }
67
68 $catalogue = new MessageCatalogue($locale);
69 $this->extract($dom, $catalogue, $domain);
70
71 if (is_file($resource) && class_exists(FileResource::class)) {
72 $catalogue->addResource(new FileResource($resource));
73 }
74
75 return $catalogue;
76 }
77
78 private function extract(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain)
79 {
80 $xliffVersion = XliffUtils::getVersionNumber($dom);
81
82 if ('1.2' === $xliffVersion) {
83 $this->extractXliff1($dom, $catalogue, $domain);
84 }
85
86 if ('2.0' === $xliffVersion) {
87 $this->extractXliff2($dom, $catalogue, $domain);
88 }
89 }
90
91 /**
92 * Extract messages and metadata from DOMDocument into a MessageCatalogue.
93 */
94 private function extractXliff1(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain)
95 {
96 $xml = simplexml_import_dom($dom);
97 $encoding = $dom->encoding ? strtoupper($dom->encoding) : null;
98
99 $namespace = 'urn:oasis:names:tc:xliff:document:1.2';
100 $xml->registerXPathNamespace('xliff', $namespace);
101
102 foreach ($xml->xpath('//xliff:file') as $file) {
103 $fileAttributes = $file->attributes();
104
105 $file->registerXPathNamespace('xliff', $namespace);
106
107 foreach ($file->xpath('.//xliff:trans-unit') as $translation) {
108 $attributes = $translation->attributes();
109
110 if (!(isset($attributes['resname']) || isset($translation->source))) {
111 continue;
112 }
113
114 $source = isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->source;
115 // If the xlf file has another encoding specified, try to convert it because
116 // simple_xml will always return utf-8 encoded values
117 $target = $this->utf8ToCharset((string) ($translation->target ?? $translation->source), $encoding);
118
119 $catalogue->set((string) $source, $target, $domain);
120
121 $metadata = [
122 'source' => (string) $translation->source,
123 'file' => [
124 'original' => (string) $fileAttributes['original'],
125 ],
126 ];
127 if ($notes = $this->parseNotesMetadata($translation->note, $encoding)) {
128 $metadata['notes'] = $notes;
129 }
130
131 if (isset($translation->target) && $translation->target->attributes()) {
132 $metadata['target-attributes'] = [];
133 foreach ($translation->target->attributes() as $key => $value) {
134 $metadata['target-attributes'][$key] = (string) $value;
135 }
136 }
137
138 if (isset($attributes['id'])) {
139 $metadata['id'] = (string) $attributes['id'];
140 }
141
142 $catalogue->setMetadata((string) $source, $metadata, $domain);
143 }
144 }
145 }
146
147 private function extractXliff2(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain)
148 {
149 $xml = simplexml_import_dom($dom);
150 $encoding = $dom->encoding ? strtoupper($dom->encoding) : null;
151
152 $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:2.0');
153
154 foreach ($xml->xpath('//xliff:unit') as $unit) {
155 foreach ($unit->segment as $segment) {
156 $attributes = $unit->attributes();
157 $source = $attributes['name'] ?? $segment->source;
158
159 // If the xlf file has another encoding specified, try to convert it because
160 // simple_xml will always return utf-8 encoded values
161 $target = $this->utf8ToCharset((string) ($segment->target ?? $segment->source), $encoding);
162
163 $catalogue->set((string) $source, $target, $domain);
164
165 $metadata = [];
166 if (isset($segment->target) && $segment->target->attributes()) {
167 $metadata['target-attributes'] = [];
168 foreach ($segment->target->attributes() as $key => $value) {
169 $metadata['target-attributes'][$key] = (string) $value;
170 }
171 }
172
173 if (isset($unit->notes)) {
174 $metadata['notes'] = [];
175 foreach ($unit->notes->note as $noteNode) {
176 $note = [];
177 foreach ($noteNode->attributes() as $key => $value) {
178 $note[$key] = (string) $value;
179 }
180 $note['content'] = (string) $noteNode;
181 $metadata['notes'][] = $note;
182 }
183 }
184
185 $catalogue->setMetadata((string) $source, $metadata, $domain);
186 }
187 }
188 }
189
190 /**
191 * Convert a UTF8 string to the specified encoding.
192 */
193 private function utf8ToCharset(string $content, string $encoding = null): string
194 {
195 if ('UTF-8' !== $encoding && !empty($encoding)) {
196 return mb_convert_encoding($content, $encoding, 'UTF-8');
197 }
198
199 return $content;
200 }
201
202 private function parseNotesMetadata(\SimpleXMLElement $noteElement = null, string $encoding = null): array
203 {
204 $notes = [];
205
206 if (null === $noteElement) {
207 return $notes;
208 }
209
210 /** @var \SimpleXMLElement $xmlNote */
211 foreach ($noteElement as $xmlNote) {
212 $noteAttributes = $xmlNote->attributes();
213 $note = ['content' => $this->utf8ToCharset((string) $xmlNote, $encoding)];
214 if (isset($noteAttributes['priority'])) {
215 $note['priority'] = (int) $noteAttributes['priority'];
216 }
217
218 if (isset($noteAttributes['from'])) {
219 $note['from'] = (string) $noteAttributes['from'];
220 }
221
222 $notes[] = $note;
223 }
224
225 return $notes;
226 }
227
228 private function isXmlString(string $resource): bool
229 {
230 return 0 === strpos($resource, '<?xml');
231 }
232}