Subversion Repositories PHPX

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
39 PointedEar 1
<?php
2
/**
3
 * Zend Framework (http://framework.zend.com/)
4
 *
5
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6
 * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
7
 * @license   http://framework.zend.com/license/new-bsd New BSD License
8
 */
9
 
10
namespace Zend\I18n\Translator;
11
 
12
use Locale;
13
use Traversable;
14
use Zend\Cache;
15
use Zend\Cache\Storage\StorageInterface as CacheStorage;
16
use Zend\I18n\Exception;
17
use Zend\I18n\Translator\Loader\FileLoaderInterface;
18
use Zend\I18n\Translator\Loader\RemoteLoaderInterface;
19
use Zend\Stdlib\ArrayUtils;
20
 
21
/**
22
 * Translator.
23
 */
24
class Translator
25
{
26
    /**
27
     * Messages loaded by the translator.
28
     *
29
     * @var array
30
     */
31
    protected $messages = array();
32
 
33
    /**
34
     * Files used for loading messages.
35
     *
36
     * @var array
37
     */
38
    protected $files = array();
39
 
40
    /**
41
     * Patterns used for loading messages.
42
     *
43
     * @var array
44
     */
45
    protected $patterns = array();
46
 
47
    /**
48
     * Remote locations for loading messages.
49
     *
50
     * @var array
51
     */
52
    protected $remote = array();
53
 
54
    /**
55
     * Default locale.
56
     *
57
     * @var string
58
     */
59
    protected $locale;
60
 
61
    /**
62
     * Locale to use as fallback if there is no translation.
63
     *
64
     * @var string
65
     */
66
    protected $fallbackLocale;
67
 
68
    /**
69
     * Translation cache.
70
     *
71
     * @var CacheStorage
72
     */
73
    protected $cache;
74
 
75
    /**
76
     * Plugin manager for translation loaders.
77
     *
78
     * @var LoaderPluginManager
79
     */
80
    protected $pluginManager;
81
 
82
    /**
83
     * Instantiate a translator
84
     *
85
     * @param  array|Traversable                  $options
86
     * @return Translator
87
     * @throws Exception\InvalidArgumentException
88
     */
89
    public static function factory($options)
90
    {
91
        if ($options instanceof Traversable) {
92
            $options = ArrayUtils::iteratorToArray($options);
93
        } elseif (!is_array($options)) {
94
            throw new Exception\InvalidArgumentException(sprintf(
95
                '%s expects an array or Traversable object; received "%s"',
96
                __METHOD__,
97
                (is_object($options) ? get_class($options) : gettype($options))
98
            ));
99
        }
100
 
101
        $translator = new static();
102
 
103
        // locales
104
        if (isset($options['locale'])) {
105
            $locales = (array) $options['locale'];
106
            $translator->setLocale(array_shift($locales));
107
            if (count($locales) > 0) {
108
                $translator->setFallbackLocale(array_shift($locales));
109
            }
110
        }
111
 
112
        // file patterns
113
        if (isset($options['translation_file_patterns'])) {
114
            if (!is_array($options['translation_file_patterns'])) {
115
                throw new Exception\InvalidArgumentException(
116
                    '"translation_file_patterns" should be an array'
117
                );
118
            }
119
 
120
            $requiredKeys = array('type', 'base_dir', 'pattern');
121
            foreach ($options['translation_file_patterns'] as $pattern) {
122
                foreach ($requiredKeys as $key) {
123
                    if (!isset($pattern[$key])) {
124
                        throw new Exception\InvalidArgumentException(
125
                            "'{$key}' is missing for translation pattern options"
126
                        );
127
                    }
128
                }
129
 
130
                $translator->addTranslationFilePattern(
131
                    $pattern['type'],
132
                    $pattern['base_dir'],
133
                    $pattern['pattern'],
134
                    isset($pattern['text_domain']) ? $pattern['text_domain'] : 'default'
135
                );
136
            }
137
        }
138
 
139
        // files
140
        if (isset($options['translation_files'])) {
141
            if (!is_array($options['translation_files'])) {
142
                throw new Exception\InvalidArgumentException(
143
                    '"translation_files" should be an array'
144
                );
145
            }
146
 
147
            $requiredKeys = array('type', 'filename');
148
            foreach ($options['translation_files'] as $file) {
149
                foreach ($requiredKeys as $key) {
150
                    if (!isset($file[$key])) {
151
                        throw new Exception\InvalidArgumentException(
152
                            "'{$key}' is missing for translation file options"
153
                        );
154
                    }
155
                }
156
 
157
                $translator->addTranslationFile(
158
                    $file['type'],
159
                    $file['filename'],
160
                    isset($file['text_domain']) ? $file['text_domain'] : 'default',
161
                    isset($file['locale']) ? $file['locale'] : null
162
                );
163
            }
164
        }
165
 
166
        // remote
167
        if (isset($options['remote_translation'])) {
168
            if (!is_array($options['remote_translation'])) {
169
                throw new Exception\InvalidArgumentException(
170
                    '"remote_translation" should be an array'
171
                );
172
            }
173
 
174
            $requiredKeys = array('type');
175
            foreach ($options['remote_translation'] as $remote) {
176
                foreach ($requiredKeys as $key) {
177
                    if (!isset($remote[$key])) {
178
                        throw new Exception\InvalidArgumentException(
179
                            "'{$key}' is missing for remote translation options"
180
                        );
181
                    }
182
                }
183
 
184
                $translator->addRemoteTranslations(
185
                    $remote['type'],
186
                    isset($remote['text_domain']) ? $remote['text_domain'] : 'default'
187
                );
188
            }
189
        }
190
 
191
        // cache
192
        if (isset($options['cache'])) {
193
            if ($options['cache'] instanceof CacheStorage) {
194
                $translator->setCache($options['cache']);
195
            } else {
196
                $translator->setCache(Cache\StorageFactory::factory($options['cache']));
197
            }
198
        }
199
 
200
        return $translator;
201
    }
202
 
203
    /**
204
     * Set the default locale.
205
     *
206
     * @param  string     $locale
207
     * @return Translator
208
     */
209
    public function setLocale($locale)
210
    {
211
        $this->locale = $locale;
212
 
213
        return $this;
214
    }
215
 
216
    /**
217
     * Get the default locale.
218
     *
219
     * @return string
220
     */
221
    public function getLocale()
222
    {
223
        if ($this->locale === null) {
224
            $this->locale = Locale::getDefault();
225
        }
226
 
227
        return $this->locale;
228
    }
229
 
230
    /**
231
     * Set the fallback locale.
232
     *
233
     * @param  string     $locale
234
     * @return Translator
235
     */
236
    public function setFallbackLocale($locale)
237
    {
238
        $this->fallbackLocale = $locale;
239
 
240
        return $this;
241
    }
242
 
243
    /**
244
     * Get the fallback locale.
245
     *
246
     * @return string
247
     */
248
    public function getFallbackLocale()
249
    {
250
        return $this->fallbackLocale;
251
    }
252
 
253
    /**
254
     * Sets a cache
255
     *
256
     * @param  CacheStorage $cache
257
     * @return Translator
258
     */
259
    public function setCache(CacheStorage $cache = null)
260
    {
261
        $this->cache = $cache;
262
 
263
        return $this;
264
    }
265
 
266
    /**
267
     * Returns the set cache
268
     *
269
     * @return CacheStorage The set cache
270
     */
271
    public function getCache()
272
    {
273
        return $this->cache;
274
    }
275
 
276
    /**
277
     * Set the plugin manager for translation loaders
278
     *
279
     * @param  LoaderPluginManager $pluginManager
280
     * @return Translator
281
     */
282
    public function setPluginManager(LoaderPluginManager $pluginManager)
283
    {
284
        $this->pluginManager = $pluginManager;
285
 
286
        return $this;
287
    }
288
 
289
    /**
290
     * Retrieve the plugin manager for translation loaders.
291
     *
292
     * Lazy loads an instance if none currently set.
293
     *
294
     * @return LoaderPluginManager
295
     */
296
    public function getPluginManager()
297
    {
298
        if (!$this->pluginManager instanceof LoaderPluginManager) {
299
            $this->setPluginManager(new LoaderPluginManager());
300
        }
301
 
302
        return $this->pluginManager;
303
    }
304
 
305
    /**
306
     * Translate a message.
307
     *
308
     * @param  string $message
309
     * @param  string $textDomain
310
     * @param  string $locale
311
     * @return string
312
     */
313
    public function translate($message, $textDomain = 'default', $locale = null)
314
    {
315
        $locale      = ($locale ?: $this->getLocale());
316
        $translation = $this->getTranslatedMessage($message, $locale, $textDomain);
317
 
318
        if ($translation !== null && $translation !== '') {
319
            return $translation;
320
        }
321
 
322
        if (null !== ($fallbackLocale = $this->getFallbackLocale())
323
            && $locale !== $fallbackLocale
324
        ) {
325
            return $this->translate($message, $textDomain, $fallbackLocale);
326
        }
327
 
328
        return $message;
329
    }
330
 
331
    /**
332
     * Translate a plural message.
333
     *
334
     * @param  string                         $singular
335
     * @param  string                         $plural
336
     * @param  int                            $number
337
     * @param  string                         $textDomain
338
     * @param  string|null                    $locale
339
     * @return string
340
     * @throws Exception\OutOfBoundsException
341
     */
342
    public function translatePlural(
343
        $singular,
344
        $plural,
345
        $number,
346
        $textDomain = 'default',
347
        $locale = null
348
    ) {
349
        $locale      = $locale ?: $this->getLocale();
350
        $translation = $this->getTranslatedMessage($singular, $locale, $textDomain);
351
 
352
        if ($translation === null || $translation === '') {
353
            if (null !== ($fallbackLocale = $this->getFallbackLocale())
354
                && $locale !== $fallbackLocale
355
            ) {
356
                return $this->translatePlural(
357
                    $singular,
358
                    $plural,
359
                    $number,
360
                    $textDomain,
361
                    $fallbackLocale
362
                );
363
            }
364
 
365
            return ($number == 1 ? $singular : $plural);
366
        }
367
 
368
        $index = $this->messages[$textDomain][$locale]
369
                      ->getPluralRule()
370
                      ->evaluate($number);
371
 
372
        if (!isset($translation[$index])) {
373
            throw new Exception\OutOfBoundsException(sprintf(
374
                'Provided index %d does not exist in plural array', $index
375
            ));
376
        }
377
 
378
        return $translation[$index];
379
    }
380
 
381
    /**
382
     * Get a translated message.
383
     *
384
     * @param  string      $message
385
     * @param  string      $locale
386
     * @param  string      $textDomain
387
     * @return string|null
388
     */
389
    protected function getTranslatedMessage(
390
        $message,
391
        $locale = null,
392
        $textDomain = 'default'
393
    ) {
394
        if ($message === '') {
395
            return '';
396
        }
397
 
398
        if (!isset($this->messages[$textDomain][$locale])) {
399
            $this->loadMessages($textDomain, $locale);
400
        }
401
 
402
        if (!isset($this->messages[$textDomain][$locale][$message])) {
403
            return null;
404
        }
405
 
406
        return $this->messages[$textDomain][$locale][$message];
407
    }
408
 
409
    /**
410
     * Add a translation file.
411
     *
412
     * @param  string     $type
413
     * @param  string     $filename
414
     * @param  string     $textDomain
415
     * @param  string     $locale
416
     * @return Translator
417
     */
418
    public function addTranslationFile(
419
        $type,
420
        $filename,
421
        $textDomain = 'default',
422
        $locale = null
423
    ) {
424
        $locale = $locale ?: '*';
425
 
426
        if (!isset($this->files[$textDomain])) {
427
            $this->files[$textDomain] = array();
428
        }
429
 
430
        $this->files[$textDomain][$locale][] = array(
431
            'type' => $type,
432
            'filename' => $filename,
433
        );
434
 
435
        return $this;
436
    }
437
 
438
    /**
439
     * Add multiple translations with a file pattern.
440
     *
441
     * @param  string     $type
442
     * @param  string     $baseDir
443
     * @param  string     $pattern
444
     * @param  string     $textDomain
445
     * @return Translator
446
     */
447
    public function addTranslationFilePattern(
448
        $type,
449
        $baseDir,
450
        $pattern,
451
        $textDomain = 'default'
452
    ) {
453
        if (!isset($this->patterns[$textDomain])) {
454
            $this->patterns[$textDomain] = array();
455
        }
456
 
457
        $this->patterns[$textDomain][] = array(
458
            'type'    => $type,
459
            'baseDir' => rtrim($baseDir, '/'),
460
            'pattern' => $pattern,
461
        );
462
 
463
        return $this;
464
    }
465
 
466
    /**
467
     * Add remote translations.
468
     *
469
     * @param  string     $type
470
     * @param  string     $textDomain
471
     * @return Translator
472
     */
473
    public function addRemoteTranslations($type, $textDomain = 'default')
474
    {
475
        if (!isset($this->remote[$textDomain])) {
476
            $this->remote[$textDomain] = array();
477
        }
478
 
479
        $this->remote[$textDomain][] = $type;
480
 
481
        return $this;
482
    }
483
 
484
    /**
485
     * Load messages for a given language and domain.
486
     *
487
     * @param  string                     $textDomain
488
     * @param  string                     $locale
489
     * @throws Exception\RuntimeException
490
     * @return void
491
     */
492
    protected function loadMessages($textDomain, $locale)
493
    {
494
        if (!isset($this->messages[$textDomain])) {
495
            $this->messages[$textDomain] = array();
496
        }
497
 
498
        if (null !== ($cache = $this->getCache())) {
499
            $cacheId = 'Zend_I18n_Translator_Messages_' . md5($textDomain . $locale);
500
 
501
            if (null !== ($result = $cache->getItem($cacheId))) {
502
                $this->messages[$textDomain][$locale] = $result;
503
 
504
                return;
505
            }
506
        }
507
 
508
        $hasToCache = false;
509
 
510
        // Try to load from remote sources
511
        if (isset($this->remote[$textDomain])) {
512
            foreach ($this->remote[$textDomain] as $loaderType) {
513
                $loader = $this->getPluginManager()->get($loaderType);
514
 
515
                if (!$loader instanceof RemoteLoaderInterface) {
516
                    throw new Exception\RuntimeException('Specified loader is not a remote loader');
517
                }
518
 
519
                if (isset($this->messages[$textDomain][$locale])) {
520
                    $this->messages[$textDomain][$locale]->exchangeArray(array_merge(
521
                        (array) $this->messages[$textDomain][$locale],
522
                        (array) $loader->load($locale, $textDomain)
523
                    ));
524
                } else {
525
                    $this->messages[$textDomain][$locale] = $loader->load($locale, $textDomain);
526
                }
527
                $hasToCache = true;
528
            }
529
        }
530
 
531
        // Try to load from pattern
532
        if (isset($this->patterns[$textDomain])) {
533
            foreach ($this->patterns[$textDomain] as $pattern) {
534
                $filename = $pattern['baseDir'] . '/' . sprintf($pattern['pattern'], $locale);
535
 
536
                if (is_file($filename)) {
537
                    $loader = $this->getPluginManager()->get($pattern['type']);
538
 
539
                    if (!$loader instanceof FileLoaderInterface) {
540
                        throw new Exception\RuntimeException('Specified loader is not a file loader');
541
                    }
542
 
543
                    if (isset($this->messages[$textDomain][$locale])) {
544
                        $this->messages[$textDomain][$locale]->exchangeArray(array_merge(
545
                            (array) $this->messages[$textDomain][$locale],
546
                            (array) $loader->load($locale, $filename)
547
                        ));
548
                    } else {
549
                        $this->messages[$textDomain][$locale] = $loader->load($locale, $filename);
550
                    }
551
                    $hasToCache = true;
552
                }
553
            }
554
        }
555
 
556
        // Try to load from concrete files
557
        foreach (array($locale, '*') as $currentLocale) {
558
            if (!isset($this->files[$textDomain][$currentLocale])) {
559
                continue;
560
            }
561
            foreach ($this->files[$textDomain][$currentLocale] as $file) {
562
                $loader = $this->getPluginManager()->get($file['type']);
563
 
564
                if (!$loader instanceof FileLoaderInterface) {
565
                    throw new Exception\RuntimeException('Specified loader is not a file loader');
566
                }
567
 
568
                if (isset($this->messages[$textDomain][$locale])) {
569
                    $this->messages[$textDomain][$locale]->exchangeArray(array_merge(
570
                        (array) $this->messages[$textDomain][$locale],
571
                        (array) $loader->load($locale, $file['filename'])
572
                    ));
573
                } else {
574
                    $this->messages[$textDomain][$locale] = $loader->load($locale, $file['filename']);
575
                }
576
                $hasToCache = true;
577
            }
578
            unset($this->files[$textDomain][$currentLocale]);
579
        }
580
 
581
        // Cache the loaded text domain
582
        if ($hasToCache && $cache !== null) {
583
            $cache->setItem($cacheId, $this->messages[$textDomain][$locale]);
584
        }
585
    }
586
}