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;
13
 
14
use Symfony\Component\Config\ConfigCacheFactory;
15
use Symfony\Component\Config\ConfigCacheFactoryInterface;
16
use Symfony\Component\Config\ConfigCacheInterface;
17
use Symfony\Component\Translation\Exception\InvalidArgumentException;
18
use Symfony\Component\Translation\Exception\NotFoundResourceException;
19
use Symfony\Component\Translation\Exception\RuntimeException;
20
use Symfony\Component\Translation\Formatter\IntlFormatterInterface;
21
use Symfony\Component\Translation\Formatter\MessageFormatter;
22
use Symfony\Component\Translation\Formatter\MessageFormatterInterface;
23
use Symfony\Component\Translation\Loader\LoaderInterface;
24
use Symfony\Contracts\Translation\LocaleAwareInterface;
25
use Symfony\Contracts\Translation\TranslatableInterface;
26
use Symfony\Contracts\Translation\TranslatorInterface;
27
 
28
// Help opcache.preload discover always-needed symbols
29
class_exists(MessageCatalogue::class);
30
 
31
/**
32
 * @author Fabien Potencier <fabien@symfony.com>
33
 */
34
class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface
35
{
36
    /**
37
     * @var MessageCatalogueInterface[]
38
     */
39
    protected $catalogues = [];
40
 
41
    private string $locale;
42
 
43
    /**
44
     * @var string[]
45
     */
46
    private array $fallbackLocales = [];
47
 
48
    /**
49
     * @var LoaderInterface[]
50
     */
51
    private array $loaders = [];
52
 
53
    private array $resources = [];
54
 
55
    private MessageFormatterInterface $formatter;
56
 
57
    private ?string $cacheDir;
58
 
59
    private bool $debug;
60
 
61
    private array $cacheVary;
62
 
63
    private ?ConfigCacheFactoryInterface $configCacheFactory;
64
 
65
    private array $parentLocales;
66
 
67
    private bool $hasIntlFormatter;
68
 
69
    /**
70
     * @throws InvalidArgumentException If a locale contains invalid characters
71
     */
72
    public function __construct(string $locale, MessageFormatterInterface $formatter = null, string $cacheDir = null, bool $debug = false, array $cacheVary = [])
73
    {
74
        $this->setLocale($locale);
75
 
76
        $this->formatter = $formatter ??= new MessageFormatter();
77
        $this->cacheDir = $cacheDir;
78
        $this->debug = $debug;
79
        $this->cacheVary = $cacheVary;
80
        $this->hasIntlFormatter = $formatter instanceof IntlFormatterInterface;
81
    }
82
 
83
    public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory)
84
    {
85
        $this->configCacheFactory = $configCacheFactory;
86
    }
87
 
88
    /**
89
     * Adds a Loader.
90
     *
91
     * @param string $format The name of the loader (@see addResource())
92
     */
93
    public function addLoader(string $format, LoaderInterface $loader)
94
    {
95
        $this->loaders[$format] = $loader;
96
    }
97
 
98
    /**
99
     * Adds a Resource.
100
     *
101
     * @param string $format   The name of the loader (@see addLoader())
102
     * @param mixed  $resource The resource name
103
     *
104
     * @throws InvalidArgumentException If the locale contains invalid characters
105
     */
106
    public function addResource(string $format, mixed $resource, string $locale, string $domain = null)
107
    {
108
        $domain ??= 'messages';
109
 
110
        $this->assertValidLocale($locale);
111
        $locale ?: $locale = class_exists(\Locale::class) ? \Locale::getDefault() : 'en';
112
 
113
        $this->resources[$locale][] = [$format, $resource, $domain];
114
 
115
        if (\in_array($locale, $this->fallbackLocales)) {
116
            $this->catalogues = [];
117
        } else {
118
            unset($this->catalogues[$locale]);
119
        }
120
    }
121
 
122
    public function setLocale(string $locale)
123
    {
124
        $this->assertValidLocale($locale);
125
        $this->locale = $locale;
126
    }
127
 
128
    public function getLocale(): string
129
    {
130
        return $this->locale ?: (class_exists(\Locale::class) ? \Locale::getDefault() : 'en');
131
    }
132
 
133
    /**
134
     * Sets the fallback locales.
135
     *
136
     * @param string[] $locales
137
     *
138
     * @throws InvalidArgumentException If a locale contains invalid characters
139
     */
140
    public function setFallbackLocales(array $locales)
141
    {
142
        // needed as the fallback locales are linked to the already loaded catalogues
143
        $this->catalogues = [];
144
 
145
        foreach ($locales as $locale) {
146
            $this->assertValidLocale($locale);
147
        }
148
 
149
        $this->fallbackLocales = $this->cacheVary['fallback_locales'] = $locales;
150
    }
151
 
152
    /**
153
     * Gets the fallback locales.
154
     *
155
     * @internal
156
     */
157
    public function getFallbackLocales(): array
158
    {
159
        return $this->fallbackLocales;
160
    }
161
 
162
    public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null): string
163
    {
164
        if (null === $id || '' === $id) {
165
            return '';
166
        }
167
 
168
        $domain ??= 'messages';
169
 
170
        $catalogue = $this->getCatalogue($locale);
171
        $locale = $catalogue->getLocale();
172
        while (!$catalogue->defines($id, $domain)) {
173
            if ($cat = $catalogue->getFallbackCatalogue()) {
174
                $catalogue = $cat;
175
                $locale = $catalogue->getLocale();
176
            } else {
177
                break;
178
            }
179
        }
180
 
181
        $parameters = array_map(function ($parameter) use ($locale) {
182
            return $parameter instanceof TranslatableInterface ? $parameter->trans($this, $locale) : $parameter;
183
        }, $parameters);
184
 
185
        $len = \strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX);
186
        if ($this->hasIntlFormatter
187
            && ($catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)
188
            || (\strlen($domain) > $len && 0 === substr_compare($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX, -$len, $len)))
189
        ) {
190
            return $this->formatter->formatIntl($catalogue->get($id, $domain), $locale, $parameters);
191
        }
192
 
193
        return $this->formatter->format($catalogue->get($id, $domain), $locale, $parameters);
194
    }
195
 
196
    public function getCatalogue(string $locale = null): MessageCatalogueInterface
197
    {
198
        if (!$locale) {
199
            $locale = $this->getLocale();
200
        } else {
201
            $this->assertValidLocale($locale);
202
        }
203
 
204
        if (!isset($this->catalogues[$locale])) {
205
            $this->loadCatalogue($locale);
206
        }
207
 
208
        return $this->catalogues[$locale];
209
    }
210
 
211
    public function getCatalogues(): array
212
    {
213
        return array_values($this->catalogues);
214
    }
215
 
216
    /**
217
     * Gets the loaders.
218
     *
219
     * @return LoaderInterface[]
220
     */
221
    protected function getLoaders(): array
222
    {
223
        return $this->loaders;
224
    }
225
 
226
    protected function loadCatalogue(string $locale)
227
    {
228
        if (null === $this->cacheDir) {
229
            $this->initializeCatalogue($locale);
230
        } else {
231
            $this->initializeCacheCatalogue($locale);
232
        }
233
    }
234
 
235
    protected function initializeCatalogue(string $locale)
236
    {
237
        $this->assertValidLocale($locale);
238
 
239
        try {
240
            $this->doLoadCatalogue($locale);
241
        } catch (NotFoundResourceException $e) {
242
            if (!$this->computeFallbackLocales($locale)) {
243
                throw $e;
244
            }
245
        }
246
        $this->loadFallbackCatalogues($locale);
247
    }
248
 
249
    private function initializeCacheCatalogue(string $locale): void
250
    {
251
        if (isset($this->catalogues[$locale])) {
252
            /* Catalogue already initialized. */
253
            return;
254
        }
255
 
256
        $this->assertValidLocale($locale);
257
        $cache = $this->getConfigCacheFactory()->cache($this->getCatalogueCachePath($locale),
258
            function (ConfigCacheInterface $cache) use ($locale) {
259
                $this->dumpCatalogue($locale, $cache);
260
            }
261
        );
262
 
263
        if (isset($this->catalogues[$locale])) {
264
            /* Catalogue has been initialized as it was written out to cache. */
265
            return;
266
        }
267
 
268
        /* Read catalogue from cache. */
269
        $this->catalogues[$locale] = include $cache->getPath();
270
    }
271
 
272
    private function dumpCatalogue(string $locale, ConfigCacheInterface $cache): void
273
    {
274
        $this->initializeCatalogue($locale);
275
        $fallbackContent = $this->getFallbackContent($this->catalogues[$locale]);
276
 
277
        $content = sprintf(<<<EOF
278
<?php
279
 
280
use Symfony\Component\Translation\MessageCatalogue;
281
 
282
\$catalogue = new MessageCatalogue('%s', %s);
283
 
284
%s
285
return \$catalogue;
286
 
287
EOF
288
            ,
289
            $locale,
290
            var_export($this->getAllMessages($this->catalogues[$locale]), true),
291
            $fallbackContent
292
        );
293
 
294
        $cache->write($content, $this->catalogues[$locale]->getResources());
295
    }
296
 
297
    private function getFallbackContent(MessageCatalogue $catalogue): string
298
    {
299
        $fallbackContent = '';
300
        $current = '';
301
        $replacementPattern = '/[^a-z0-9_]/i';
302
        $fallbackCatalogue = $catalogue->getFallbackCatalogue();
303
        while ($fallbackCatalogue) {
304
            $fallback = $fallbackCatalogue->getLocale();
305
            $fallbackSuffix = ucfirst(preg_replace($replacementPattern, '_', $fallback));
306
            $currentSuffix = ucfirst(preg_replace($replacementPattern, '_', $current));
307
 
308
            $fallbackContent .= sprintf(<<<'EOF'
309
$catalogue%s = new MessageCatalogue('%s', %s);
310
$catalogue%s->addFallbackCatalogue($catalogue%s);
311
 
312
EOF
313
                ,
314
                $fallbackSuffix,
315
                $fallback,
316
                var_export($this->getAllMessages($fallbackCatalogue), true),
317
                $currentSuffix,
318
                $fallbackSuffix
319
            );
320
            $current = $fallbackCatalogue->getLocale();
321
            $fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue();
322
        }
323
 
324
        return $fallbackContent;
325
    }
326
 
327
    private function getCatalogueCachePath(string $locale): string
328
    {
329
        return $this->cacheDir.'/catalogue.'.$locale.'.'.strtr(substr(base64_encode(hash('sha256', serialize($this->cacheVary), true)), 0, 7), '/', '_').'.php';
330
    }
331
 
332
    /**
333
     * @internal
334
     */
335
    protected function doLoadCatalogue(string $locale): void
336
    {
337
        $this->catalogues[$locale] = new MessageCatalogue($locale);
338
 
339
        if (isset($this->resources[$locale])) {
340
            foreach ($this->resources[$locale] as $resource) {
341
                if (!isset($this->loaders[$resource[0]])) {
342
                    if (\is_string($resource[1])) {
343
                        throw new RuntimeException(sprintf('No loader is registered for the "%s" format when loading the "%s" resource.', $resource[0], $resource[1]));
344
                    }
345
 
346
                    throw new RuntimeException(sprintf('No loader is registered for the "%s" format.', $resource[0]));
347
                }
348
                $this->catalogues[$locale]->addCatalogue($this->loaders[$resource[0]]->load($resource[1], $locale, $resource[2]));
349
            }
350
        }
351
    }
352
 
353
    private function loadFallbackCatalogues(string $locale): void
354
    {
355
        $current = $this->catalogues[$locale];
356
 
357
        foreach ($this->computeFallbackLocales($locale) as $fallback) {
358
            if (!isset($this->catalogues[$fallback])) {
359
                $this->initializeCatalogue($fallback);
360
            }
361
 
362
            $fallbackCatalogue = new MessageCatalogue($fallback, $this->getAllMessages($this->catalogues[$fallback]));
363
            foreach ($this->catalogues[$fallback]->getResources() as $resource) {
364
                $fallbackCatalogue->addResource($resource);
365
            }
366
            $current->addFallbackCatalogue($fallbackCatalogue);
367
            $current = $fallbackCatalogue;
368
        }
369
    }
370
 
371
    protected function computeFallbackLocales(string $locale)
372
    {
373
        $this->parentLocales ??= json_decode(file_get_contents(__DIR__.'/Resources/data/parents.json'), true);
374
 
375
        $originLocale = $locale;
376
        $locales = [];
377
 
378
        while ($locale) {
379
            $parent = $this->parentLocales[$locale] ?? null;
380
 
381
            if ($parent) {
382
                $locale = 'root' !== $parent ? $parent : null;
383
            } elseif (\function_exists('locale_parse')) {
384
                $localeSubTags = locale_parse($locale);
385
                $locale = null;
386
                if (1 < \count($localeSubTags)) {
387
                    array_pop($localeSubTags);
388
                    $locale = locale_compose($localeSubTags) ?: null;
389
                }
390
            } elseif ($i = strrpos($locale, '_') ?: strrpos($locale, '-')) {
391
                $locale = substr($locale, 0, $i);
392
            } else {
393
                $locale = null;
394
            }
395
 
396
            if (null !== $locale) {
397
                $locales[] = $locale;
398
            }
399
        }
400
 
401
        foreach ($this->fallbackLocales as $fallback) {
402
            if ($fallback === $originLocale) {
403
                continue;
404
            }
405
 
406
            $locales[] = $fallback;
407
        }
408
 
409
        return array_unique($locales);
410
    }
411
 
412
    /**
413
     * Asserts that the locale is valid, throws an Exception if not.
414
     *
415
     * @throws InvalidArgumentException If the locale contains invalid characters
416
     */
417
    protected function assertValidLocale(string $locale)
418
    {
419
        if (!preg_match('/^[a-z0-9@_\\.\\-]*$/i', $locale)) {
420
            throw new InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale));
421
        }
422
    }
423
 
424
    /**
425
     * Provides the ConfigCache factory implementation, falling back to a
426
     * default implementation if necessary.
427
     */
428
    private function getConfigCacheFactory(): ConfigCacheFactoryInterface
429
    {
430
        $this->configCacheFactory ??= new ConfigCacheFactory($this->debug);
431
 
432
        return $this->configCacheFactory;
433
    }
434
 
435
    private function getAllMessages(MessageCatalogueInterface $catalogue): array
436
    {
437
        $allMessages = [];
438
 
439
        foreach ($catalogue->all() as $domain => $messages) {
440
            if ($intlMessages = $catalogue->all($domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)) {
441
                $allMessages[$domain.MessageCatalogue::INTL_DOMAIN_SUFFIX] = $intlMessages;
442
                $messages = array_diff_key($messages, $intlMessages);
443
            }
444
            if ($messages) {
445
                $allMessages[$domain] = $messages;
446
            }
447
        }
448
 
449
        return $allMessages;
450
    }
451
}