Completed
Branch feature/pre-split (775c2c)
by Anton
02:48
created

Translator::get()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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