Completed
Branch 09branch (740a7d)
by Anton
02:52
created

Translator::withLocale()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 1
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A Translator::setLocale() 0 10 2
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Translator;
10
11
use Spiral\Core\Component;
12
use Spiral\Core\Container\SingletonInterface;
13
use Spiral\Core\MemoryInterface;
14
use Spiral\Core\NullMemory;
15
use Spiral\Debug\Traits\BenchmarkTrait;
16
use Spiral\Translator\Configs\TranslatorConfig;
17
use Spiral\Translator\Exceptions\LocaleException;
18
use Spiral\Translator\Exceptions\PluralizationException;
19
use Symfony\Component\Translation\MessageSelector;
20
21
/**
22
 * Simple implementation of Symfony\TranslatorInterface with memory caching and automatic message
23
 * registration.
24
 */
25
class Translator extends Component implements SingletonInterface, TranslatorInterface
26
{
27
    use BenchmarkTrait;
28
29
    /**
30
     * Memory section.
31
     */
32
    const MEMORY = 'translator';
33
34
    /**
35
     * @var TranslatorConfig
36
     */
37
    private $config = null;
38
39
    /**
40
     * Symfony selection logic is little
41
     *
42
     * @var MessageSelector
43
     */
44
    private $selector = null;
45
46
    /**
47
     * Current locale.
48
     *
49
     * @var string
50
     */
51
    private $locale = '';
52
53
    /**
54
     * Loaded catalogues (hash).
55
     *
56
     * @var Catalogue[]
57
     */
58
    private $catalogues = [];
59
60
    /**
61
     * @var array
62
     */
63
    private $loadedLocales = [];
64
65
    /**
66
     * Catalogue to be used for fallback translation.
67
     *
68
     * @var Catalogue|null
69
     */
70
    private $fallback = null;
71
72
    /**
73
     * To load locale data from application files.
74
     *
75
     * @var LocatorInterface
76
     */
77
    protected $source = null;
78
79
    /**
80
     * @var MemoryInterface
81
     */
82
    protected $memory = null;
83
84
    /**
85
     * @param TranslatorConfig $config
86
     * @param LocatorInterface $locator
87
     * @param MemoryInterface  $memory
88
     * @param MessageSelector  $selector
89
     */
90
    public function __construct(
91
        TranslatorConfig $config,
92
93
        LocatorInterface $locator,
94
        MemoryInterface $memory = null,
95
        MessageSelector $selector = null
96
    ) {
97
        $this->config = $config;
98
        $this->source = $locator;
99
100
        $this->memory = $memory ?? new NullMemory();
101
        $this->selector = $selector ?? new MessageSelector();
102
103
        $this->locale = $this->config->defaultLocale();
104
105
        //List of known and loaded locales (loading can be delayed)
106
        $this->loadedLocales = (array)$this->memory->loadData(static::MEMORY);
107
    }
108
109
    /**
110
     * @return LocatorInterface
111
     */
112
    public function getSource(): LocatorInterface
113
    {
114
        return $this->source;
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120
    public function resolveDomain(string $bundle): string
121
    {
122
        return $this->config->resolveDomain($bundle);
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     *
128
     * Non immutable version of withLocale.
129
     *
130
     * @return $this
131
     *
132
     * @throws LocaleException
133
     */
134
    public function setLocale($locale)
135
    {
136
        if (!$this->hasLocale($locale)) {
137
            throw new LocaleException($locale);
138
        }
139
140
        $this->locale = $locale;
141
142
        return $this;
143
    }
144
145
    /**
146
     * @return string
147
     */
148
    public function getLocale(): string
149
    {
150
        return $this->locale;
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     *
156
     * Parameters will be embedded into string using { and } braces.
157
     *
158
     * @throws LocaleException
159
     */
160
    public function trans($id, array $parameters = [], $domain = null, $locale = null)
161
    {
162
        $domain = $domain ?? $this->config->defaultDomain();
163
        $locale = $locale ?? $this->locale;
164
165
        //Automatically falls back to default locale
166
        $translation = $this->get($domain, $id, $locale);
167
168
        return \Spiral\interpolate($translation, $parameters);
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     *
174
     * Default symfony pluralizer to be used. Parameters will be embedded into string using { and }
175
     * braces. In addition you can use forced parameter {n} which contain formatted number value.
176
     *
177
     * @throws LocaleException
178
     * @throws PluralizationException
179
     */
180
    public function transChoice(
181
        $id,
182
        $number,
183
        array $parameters = [],
184
        $domain = null,
185
        $locale = null
186
    ) {
187
        $domain = $domain ?? $this->config->defaultDomain();
188
        $locale = $locale ?? $this->locale;
189
190
        if (empty($parameters['{n}'])) {
191
            $parameters['{n}'] = number_format($number);
192
        }
193
194
        //Automatically falls back to default locale
195
        $translation = $this->get($domain, $id, $locale);
196
197
        try {
198
            $pluralized = $this->selector->choose($translation, $number, $locale);
199
        } catch (\InvalidArgumentException $e) {
200
            //Wrapping into more explanatory exception
201
            throw new PluralizationException($e->getMessage(), $e->getCode(), $e);
202
        }
203
204
        return \Spiral\interpolate($pluralized, $parameters);
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210
    public function getLocales(): array
211
    {
212
        if (!empty($this->loadedLocales)) {
213
            return array_keys($this->loadedLocales);
214
        }
215
216
        return $this->source->getLocales();
217
    }
218
219
    /**
220
     * Return catalogue for specific locate or return default one if no locale specified.
221
     *
222
     * @param string $locale
223
     *
224
     * @return Catalogue
225
     *
226
     * @throws LocaleException
227
     */
228
    public function getCatalogue(string $locale = null): Catalogue
229
    {
230
        if (empty($locale)) {
231
            $locale = $this->locale;
232
        }
233
234
        if (!$this->hasLocale($locale)) {
235
            throw new LocaleException("Undefined locale '{$locale}'");
236
        }
237
238
        if (!isset($this->catalogues[$locale])) {
239
            $this->catalogues[$locale] = $this->loadCatalogue($locale);
240
        }
241
242
        return $this->catalogues[$locale];
243
    }
244
245
    /**
246
     * Load all possible locales into memory.
247
     *
248
     * @return self
249
     */
250
    public function loadLocales(): Translator
251
    {
252
        foreach ($this->source->getLocales() as $locale) {
253
            $this->loadCatalogue($locale);
254
        }
255
256
        return $this;
257
    }
258
259
    /**
260
     * Flush all loaded locales data.
261
     *
262
     * @return self
263
     */
264
    public function flushLocales(): Translator
265
    {
266
        $this->loadedLocales = [];
267
        $this->catalogues = [];
268
269
        $this->memory->saveData(static::MEMORY, []);
270
271
        //Flushing fallback catalogue
272
        $this->fallback = null;
273
274
        return $this;
275
    }
276
277
    /**
278
     * Get message from specific locale, add it into fallback locale cache (to be later exported) if
279
     * enabled (see TranslatorConfig) and no translations found.
280
     *
281
     * @param string $domain
282
     * @param string $string
283
     * @param string $locale
284
     *
285
     * @return string
286
     */
287
    protected function get(string $domain, string $string, string $locale): string
288
    {
289
        //Active language first
290
        if ($this->getCatalogue($locale)->has($domain, $string)) {
291
            return $this->getCatalogue($locale)->get($domain, $string);
292
        }
293
294
        $fallback = $this->fallbackCatalogue();
295
296
        if ($fallback->has($domain, $string)) {
297
            return $fallback->get($domain, $string);
298
        }
299
300
        //Automatic message registration.
301
        if ($this->config->registerMessages()) {
302
            $fallback->set($domain, $string, $string);
303
            $fallback->saveDomains();
304
        }
305
306
        //Unable to find translation
307
        return $string;
308
    }
309
310
    /**
311
     * @return Catalogue
312
     */
313
    protected function fallbackCatalogue(): Catalogue
314
    {
315
        if (empty($this->fallback)) {
316
            $this->fallback = $this->loadCatalogue($this->config->fallbackLocale());
317
        }
318
319
        return $this->fallback;
320
    }
321
322
    /**
323
     * Load catalogue data from source.
324
     *
325
     * @param string $locale
326
     *
327
     * @return Catalogue
328
     */
329
    protected function loadCatalogue(string $locale): Catalogue
330
    {
331
        $catalogue = new Catalogue($locale, $this->memory);
332
333
        if (array_key_exists($locale, $this->loadedLocales) && $this->config->cacheLocales()) {
334
335
            //Has been loaded
336
            $catalogue->loadDomains($this->loadedLocales[$locale]);
337
338
            return $catalogue;
339
        }
340
341
        $benchmark = $this->benchmark('load', $locale);
342
        try {
343
344
            //Loading catalogue data from source
345
            foreach ($this->source->loadLocale($locale) as $messageCatalogue) {
346
                $catalogue->mergeFrom($messageCatalogue);
347
            }
348
349
            //To remember that locale already loaded
350
            $this->loadedLocales[$locale] = $catalogue->loadedDomains();
351
            $this->memory->saveData(static::MEMORY, $this->loadedLocales);
352
353
            //Saving domains memory
354
            $catalogue->saveDomains();
355
        } finally {
356
            $this->benchmark($benchmark);
357
        }
358
359
        return $catalogue;
360
    }
361
362
    /**
363
     * Check if given locale exists.
364
     *
365
     * @param string $locale
366
     *
367
     * @return bool
368
     */
369
    private function hasLocale(string $locale): bool
370
    {
371
        if (array_key_exists($locale, $this->loadedLocales)) {
372
            return true;
373
        }
374
375
        return $this->source->hasLocale($locale);
376
    }
377
378
    /**
379
     * Check if string has translation braces [[ and ]].
380
     *
381
     * @param string $string
382
     *
383
     * @return bool
384
     */
385
    public static function isMessage(string $string): bool
386
    {
387
        return substr($string, 0, 2) == self::I18N_PREFIX
388
            && substr($string, -2) == self::I18N_POSTFIX;
389
    }
390
}