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
12namespace Symfony\Component\Translation;
13
14use Symfony\Component\Config\Resource\ResourceInterface;
15use Symfony\Component\Translation\Exception\LogicException;
16
17/**
18 * @author Fabien Potencier <fabien@symfony.com>
19 */
20class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterface
21{
22    private $messages = [];
23    private $metadata = [];
24    private $resources = [];
25    private $locale;
26    private $fallbackCatalogue;
27    private $parent;
28
29    /**
30     * @param string $locale   The locale
31     * @param array  $messages An array of messages classified by domain
32     */
33    public function __construct(?string $locale, array $messages = [])
34    {
35        if (null === $locale) {
36            @trigger_error(sprintf('Passing "null" to the first argument of the "%s" method has been deprecated since Symfony 4.4 and will throw an error in 5.0.', __METHOD__), \E_USER_DEPRECATED);
37        }
38
39        $this->locale = $locale;
40        $this->messages = $messages;
41    }
42
43    /**
44     * {@inheritdoc}
45     */
46    public function getLocale()
47    {
48        return $this->locale;
49    }
50
51    /**
52     * {@inheritdoc}
53     */
54    public function getDomains()
55    {
56        $domains = [];
57        $suffixLength = \strlen(self::INTL_DOMAIN_SUFFIX);
58
59        foreach ($this->messages as $domain => $messages) {
60            if (\strlen($domain) > $suffixLength && false !== $i = strpos($domain, self::INTL_DOMAIN_SUFFIX, -$suffixLength)) {
61                $domain = substr($domain, 0, $i);
62            }
63            $domains[$domain] = $domain;
64        }
65
66        return array_values($domains);
67    }
68
69    /**
70     * {@inheritdoc}
71     */
72    public function all($domain = null)
73    {
74        if (null !== $domain) {
75            // skip messages merge if intl-icu requested explicitly
76            if (false !== strpos($domain, self::INTL_DOMAIN_SUFFIX)) {
77                return $this->messages[$domain] ?? [];
78            }
79
80            return ($this->messages[$domain.self::INTL_DOMAIN_SUFFIX] ?? []) + ($this->messages[$domain] ?? []);
81        }
82
83        $allMessages = [];
84        $suffixLength = \strlen(self::INTL_DOMAIN_SUFFIX);
85
86        foreach ($this->messages as $domain => $messages) {
87            if (\strlen($domain) > $suffixLength && false !== $i = strpos($domain, self::INTL_DOMAIN_SUFFIX, -$suffixLength)) {
88                $domain = substr($domain, 0, $i);
89                $allMessages[$domain] = $messages + ($allMessages[$domain] ?? []);
90            } else {
91                $allMessages[$domain] = ($allMessages[$domain] ?? []) + $messages;
92            }
93        }
94
95        return $allMessages;
96    }
97
98    /**
99     * {@inheritdoc}
100     */
101    public function set($id, $translation, $domain = 'messages')
102    {
103        $this->add([$id => $translation], $domain);
104    }
105
106    /**
107     * {@inheritdoc}
108     */
109    public function has($id, $domain = 'messages')
110    {
111        if (isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) {
112            return true;
113        }
114
115        if (null !== $this->fallbackCatalogue) {
116            return $this->fallbackCatalogue->has($id, $domain);
117        }
118
119        return false;
120    }
121
122    /**
123     * {@inheritdoc}
124     */
125    public function defines($id, $domain = 'messages')
126    {
127        return isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id]);
128    }
129
130    /**
131     * {@inheritdoc}
132     */
133    public function get($id, $domain = 'messages')
134    {
135        if (isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) {
136            return $this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id];
137        }
138
139        if (isset($this->messages[$domain][$id])) {
140            return $this->messages[$domain][$id];
141        }
142
143        if (null !== $this->fallbackCatalogue) {
144            return $this->fallbackCatalogue->get($id, $domain);
145        }
146
147        return $id;
148    }
149
150    /**
151     * {@inheritdoc}
152     */
153    public function replace($messages, $domain = 'messages')
154    {
155        unset($this->messages[$domain], $this->messages[$domain.self::INTL_DOMAIN_SUFFIX]);
156
157        $this->add($messages, $domain);
158    }
159
160    /**
161     * {@inheritdoc}
162     */
163    public function add($messages, $domain = 'messages')
164    {
165        if (!isset($this->messages[$domain])) {
166            $this->messages[$domain] = [];
167        }
168        $intlDomain = $domain;
169        $suffixLength = \strlen(self::INTL_DOMAIN_SUFFIX);
170        if (\strlen($domain) < $suffixLength || false === strpos($domain, self::INTL_DOMAIN_SUFFIX, -$suffixLength)) {
171            $intlDomain .= self::INTL_DOMAIN_SUFFIX;
172        }
173        foreach ($messages as $id => $message) {
174            if (isset($this->messages[$intlDomain]) && \array_key_exists($id, $this->messages[$intlDomain])) {
175                $this->messages[$intlDomain][$id] = $message;
176            } else {
177                $this->messages[$domain][$id] = $message;
178            }
179        }
180    }
181
182    /**
183     * {@inheritdoc}
184     */
185    public function addCatalogue(MessageCatalogueInterface $catalogue)
186    {
187        if ($catalogue->getLocale() !== $this->locale) {
188            throw new LogicException(sprintf('Cannot add a catalogue for locale "%s" as the current locale for this catalogue is "%s".', $catalogue->getLocale(), $this->locale));
189        }
190
191        foreach ($catalogue->all() as $domain => $messages) {
192            if ($intlMessages = $catalogue->all($domain.self::INTL_DOMAIN_SUFFIX)) {
193                $this->add($intlMessages, $domain.self::INTL_DOMAIN_SUFFIX);
194                $messages = array_diff_key($messages, $intlMessages);
195            }
196            $this->add($messages, $domain);
197        }
198
199        foreach ($catalogue->getResources() as $resource) {
200            $this->addResource($resource);
201        }
202
203        if ($catalogue instanceof MetadataAwareInterface) {
204            $metadata = $catalogue->getMetadata('', '');
205            $this->addMetadata($metadata);
206        }
207    }
208
209    /**
210     * {@inheritdoc}
211     */
212    public function addFallbackCatalogue(MessageCatalogueInterface $catalogue)
213    {
214        // detect circular references
215        $c = $catalogue;
216        while ($c = $c->getFallbackCatalogue()) {
217            if ($c->getLocale() === $this->getLocale()) {
218                throw new LogicException(sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale()));
219            }
220        }
221
222        $c = $this;
223        do {
224            if ($c->getLocale() === $catalogue->getLocale()) {
225                throw new LogicException(sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale()));
226            }
227
228            foreach ($catalogue->getResources() as $resource) {
229                $c->addResource($resource);
230            }
231        } while ($c = $c->parent);
232
233        $catalogue->parent = $this;
234        $this->fallbackCatalogue = $catalogue;
235
236        foreach ($catalogue->getResources() as $resource) {
237            $this->addResource($resource);
238        }
239    }
240
241    /**
242     * {@inheritdoc}
243     */
244    public function getFallbackCatalogue()
245    {
246        return $this->fallbackCatalogue;
247    }
248
249    /**
250     * {@inheritdoc}
251     */
252    public function getResources()
253    {
254        return array_values($this->resources);
255    }
256
257    /**
258     * {@inheritdoc}
259     */
260    public function addResource(ResourceInterface $resource)
261    {
262        $this->resources[$resource->__toString()] = $resource;
263    }
264
265    /**
266     * {@inheritdoc}
267     */
268    public function getMetadata($key = '', $domain = 'messages')
269    {
270        if ('' == $domain) {
271            return $this->metadata;
272        }
273
274        if (isset($this->metadata[$domain])) {
275            if ('' == $key) {
276                return $this->metadata[$domain];
277            }
278
279            if (isset($this->metadata[$domain][$key])) {
280                return $this->metadata[$domain][$key];
281            }
282        }
283
284        return null;
285    }
286
287    /**
288     * {@inheritdoc}
289     */
290    public function setMetadata($key, $value, $domain = 'messages')
291    {
292        $this->metadata[$domain][$key] = $value;
293    }
294
295    /**
296     * {@inheritdoc}
297     */
298    public function deleteMetadata($key = '', $domain = 'messages')
299    {
300        if ('' == $domain) {
301            $this->metadata = [];
302        } elseif ('' == $key) {
303            unset($this->metadata[$domain]);
304        } else {
305            unset($this->metadata[$domain][$key]);
306        }
307    }
308
309    /**
310     * Adds current values with the new values.
311     *
312     * @param array $values Values to add
313     */
314    private function addMetadata(array $values)
315    {
316        foreach ($values as $domain => $keys) {
317            foreach ($keys as $key => $value) {
318                $this->setMetadata($key, $value, $domain);
319            }
320        }
321    }
322}
323