Completed
Pull Request — master (#171)
by Anton
02:55
created

Translator::syncLocales()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
rs 9.4285
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\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
 * Implementation of Symfony\TranslatorInterface with memory caching and automatic message
23
 * registration.
24
 *
25
 * Immutable version is required.
26
 */
27
class Translator extends Component implements SingletonInterface, TranslatorInterface
28
{
29
    use BenchmarkTrait;
30
31
    /**
32
     * Memory section.
33
     */
34
    const MEMORY = 'translator';
35
36
    /**
37
     * @var TranslatorConfig
38
     */
39
    private $config = null;
40
41
    /**
42
     * Symfony selection logic is little
43
     *
44
     * @var MessageSelector
45
     */
46
    private $selector = null;
47
48
    /**
49
     * Current locale.
50
     *
51
     * @var string
52
     */
53
    private $locale = '';
54
55
    /**
56
     * Loaded catalogues (hash).
57
     *
58
     * @var Catalogue[]
59
     */
60
    private $catalogues = [];
61
62
    /**
63
     * @var array
64
     */
65
    private $loadedLocales = [];
66
67
    /**
68
     * Catalogue to be used for fallback translation.
69
     *
70
     * @var Catalogue|null
71
     */
72
    private $fallback = null;
73
74
    /**
75
     * To load locale data from application files.
76
     *
77
     * @var LocatorInterface
78
     */
79
    protected $source = null;
80
81
    /**
82
     * @var MemoryInterface
83
     */
84
    protected $memory = null;
85
86
    /**
87
     * @param TranslatorConfig $config
88
     * @param LocatorInterface $locator
89
     * @param MemoryInterface  $memory
90
     * @param MessageSelector  $selector
91
     */
92
    public function __construct(
93
        TranslatorConfig $config,
94
95
        LocatorInterface $locator,
96
        MemoryInterface $memory = null,
97
        MessageSelector $selector = null
98
    ) {
99
        $this->config = $config;
100
        $this->source = $locator;
101
102
        $this->memory = $memory ?? new NullMemory();
103
        $this->selector = $selector ?? new MessageSelector();
104
105
        $this->locale = $this->config->defaultLocale();
106
107
        //List of known and loaded locales (loading can be delayed)
108
        $this->loadedLocales = (array)$this->memory->loadData(static::MEMORY);
109
    }
110
111
    /**
112
     * @return LocatorInterface
113
     */
114
    public function getSource(): LocatorInterface
115
    {
116
        return $this->source;
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122
    public function resolveDomain(string $bundle): string
123
    {
124
        return $this->config->resolveDomain($bundle);
125
    }
126
127
    /**
128
     * {@inheritdoc}
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
     * Sync domains created in a current session with static memory, calling this method is required
279
     * after indexation.
280
     */
281
    public function syncLocales()
282
    {
283
        foreach ($this->catalogues as $locale => $catalogue) {
284
            $this->loadedLocales[$locale] = $catalogue->loadedDomains();
285
            $this->memory->saveData(static::MEMORY, $this->loadedLocales);
286
        }
287
    }
288
289
    /**
290
     * Get message from specific locale, add it into fallback locale cache (to be later exported) if
291
     * enabled (see TranslatorConfig) and no translations found.
292
     *
293
     * @param string $domain
294
     * @param string $string
295
     * @param string $locale
296
     *
297
     * @return string
298
     */
299
    protected function get(string $domain, string $string, string $locale): string
300
    {
301
        //Active language first
302
        if ($this->getCatalogue($locale)->has($domain, $string)) {
303
            return $this->getCatalogue($locale)->get($domain, $string);
304
        }
305
306
        $fallback = $this->fallbackCatalogue();
307
308
        if ($fallback->has($domain, $string)) {
309
            return $fallback->get($domain, $string);
310
        }
311
312
        //Automatic message registration.
313
        if ($this->config->registerMessages()) {
314
            $fallback->set($domain, $string, $string);
315
            $fallback->saveDomains();
316
        }
317
318
        //Unable to find translation
319
        return $string;
320
    }
321
322
    /**
323
     * @return Catalogue
324
     */
325
    protected function fallbackCatalogue(): Catalogue
326
    {
327
        if (empty($this->fallback)) {
328
            $this->fallback = $this->loadCatalogue($this->config->fallbackLocale());
329
        }
330
331
        return $this->fallback;
332
    }
333
334
    /**
335
     * Load catalogue data from source.
336
     *
337
     * @param string $locale
338
     *
339
     * @return Catalogue
340
     */
341
    protected function loadCatalogue(string $locale): Catalogue
342
    {
343
        $catalogue = new Catalogue($locale, $this->memory);
344
345
        if (array_key_exists($locale, $this->loadedLocales) && $this->config->cacheLocales()) {
346
347
            //Has been loaded
348
            $catalogue->loadDomains($this->loadedLocales[$locale]);
349
350
            return $catalogue;
351
        }
352
353
        $benchmark = $this->benchmark('load', $locale);
354
        try {
355
356
            //Loading catalogue data from source
357
            foreach ($this->source->loadLocale($locale) as $messageCatalogue) {
358
                $catalogue->mergeFrom($messageCatalogue);
359
            }
360
361
            //To remember that locale already loaded
362
            $this->loadedLocales[$locale] = $catalogue->loadedDomains();
363
            $this->memory->saveData(static::MEMORY, $this->loadedLocales);
364
365
            //Saving domains memory
366
            $catalogue->saveDomains();
367
        } finally {
368
            $this->benchmark($benchmark);
369
        }
370
371
        return $catalogue;
372
    }
373
374
    /**
375
     * Check if given locale exists.
376
     *
377
     * @param string $locale
378
     *
379
     * @return bool
380
     */
381
    private function hasLocale(string $locale): bool
382
    {
383
        if (array_key_exists($locale, $this->loadedLocales)) {
384
            return true;
385
        }
386
387
        return $this->source->hasLocale($locale);
388
    }
389
390
    /**
391
     * Check if string has translation braces [[ and ]].
392
     *
393
     * @param string $string
394
     *
395
     * @return bool
396
     */
397
    public static function isMessage(string $string): bool
398
    {
399
        return substr($string, 0, 2) == self::I18N_PREFIX
400
            && substr($string, -2) == self::I18N_POSTFIX;
401
    }
402
}