Issues (36)

src/Localization.php (2 issues)

1
<?php
2
/**
3
 * File containing the {@link Localization} class.
4
 * @package Localization
5
 * @subpackage Core
6
 * @see Localization
7
 */
8
9
declare(strict_types=1);
10
11
namespace AppLocalize;
12
13
use AppLocalize\Localization\Countries\BaseCountry;
14
use AppLocalize\Localization\Countries\CountryCollection;
15
use AppLocalize\Localization\Currencies\CurrencyCollection;
16
use AppLocalize\Localization\Currencies\CurrencyInterface;
17
use AppLocalize\Localization\Event\LocaleChanged;
18
use AppUtils\ClassHelper;
19
use AppUtils\FileHelper;
20
use AppUtils\FileHelper\FileInfo;
21
use AppUtils\FileHelper_Exception;
22
use HTML_QuickForm2_Container;
23
use HTML_QuickForm2_Element_Select;
24
use Mistralys\ChangelogParser\ChangelogParser;
25
use Throwable;
26
27
/**
28
 * Localization handling collection for both the
29
 * application itself and its user contents.
30
 *
31
 * @package Localization
32
 * @subpackage Core
33
 * @author Sebastian Mordziol <[email protected]>
34
 */
35
class Localization
36
{
37
    const ERROR_UNKNOWN_CONTENT_LOCALE = 39001;
38
    const ERROR_UNKNOWN_APPLICATION_LOCALE = 39002;
39
    const ERROR_NO_STORAGE_FILE_SET = 39003;
40
    const ERROR_CONFIGURE_NOT_CALLED = 39004;
41
    const ERROR_NO_SOURCES_ADDED = 39005;
42
    const ERROR_NO_LOCALE_SELECTED_IN_NS = 39006;
43
    const ERROR_NO_LOCALES_IN_NAMESPACE = 39007;
44
    const ERROR_UNKNOWN_NAMESPACE = 39008;
45
    const ERROR_UNKNOWN_LOCALE_IN_NS = 39009;
46
    const ERROR_UNKNOWN_EVENT_NAME = 39010;
47
    const ERROR_LOCALE_NOT_FOUND = 39011;
48
    const ERROR_COUNTRY_NOT_FOUND = 39012;
49
    
50
    /**
51
     * The name of the default application locale, i.e. the
52
     * locale in which application textual content is written.
53
     *
54
     * @var string
55
     */
56
    const BUILTIN_LOCALE_NAME = 'en_UK';
57
58
    const NAMESPACE_APPLICATION = '__application';
59
    const NAMESPACE_CONTENT = '__content';
60
61
    public const EVENT_LOCALE_CHANGED = 'LocaleChanged';
62
    public const EVENT_CLIENT_FOLDER_CHANGED = 'ClientFolderChanged';
63
    public const EVENT_CACHE_KEY_CHANGED = 'CacheKeyChanged';
64
65
    /**
66
    * Collection of all locales by namespace (application, content, custom...). 
67
    *
68
    * @var array<string,array<string,Localization_Locale>>
69
    * @see Localization::addLocale()
70
    */
71
    protected static $locales = array();
72
73
    /**
74
     * @var boolean
75
     * @see Localization::init()
76
     */
77
    private static $initDone = false;
78
79
   /**
80
    * Path to the file in which the scanner results are stored.
81
    * @var string
82
    * @see Localization::configure()
83
    */
84
    protected static $storageFile = '';
85
    
86
   /**
87
    * Path to the folder into which the client libraries are written.
88
    * @var string
89
    * @see Localization::setClientLibrariesFolder()
90
    */
91
    protected static $clientFolder = '';
92
    
93
   /**
94
    * If this key changes, client libraries are refreshed.
95
    * @var string
96
    * @see Localization::setClientLibrariesCacheKey()
97
    */
98
    protected static $clientCacheKey = '';
99
    
100
   /**
101
    * Whether the configuration has been made.
102
    * @var bool
103
    * @see Localization::configure()
104
    */
105
    protected static $configured = false;
106
    
107
   /**
108
    * Stores event listener instances.
109
    * @var array
110
    */
111
    protected static array $listeners = array();
112
    
113
   /**
114
    * @var integer
115
    * @see Localization::addEventListener()
116
    */
117
    protected static $listenersCounter = 0;
118
    
119
   /**
120
    * @var Localization_Translator|NULL
121
    */
122
    protected static $translator;
123
    
124
   /**
125
    * Initializes the localization layer. This is done
126
    * automatically, and only once per request.
127
    * 
128
    * (Called at the end of this file)
129
    */
130
    public static function init() : void
131
    {
132
        if(self::$initDone) {
133
            return;
134
        }
135
136
        self::reset();
137
        
138
        $installFolder = realpath(__DIR__.'/../');
139
        
140
        // add the localization package's own sources,
141
        // so the bundled localized strings can
142
        // always be translated.
143
        Localization::addSourceFolder(
144
            'application-localization',
145
            'Application Localization Package',
146
            'Composer packages',
147
            $installFolder.'/localization',
148
            $installFolder.'/src'
149
        )
150
        ->excludeFiles(array('jtokenizer'))
151
        ->excludeFolder('css');
152
        
153
        self::$initDone = true;
154
    }
155
156
    /**
157
     * Retrieves all available application locales, as an indexed
158
     * array with locale objects sorted by locale label.
159
     *
160
     * @return Localization_Locale[]
161
     * @see getAppLocale()
162
     */
163
    public static function getAppLocales()
164
    {
165
        return self::getLocalesByNS(self::NAMESPACE_APPLICATION);
166
    }
167
    
168
   /**
169
    * Retrieves all locales in the specified namespace.
170
    * 
171
    * @param string $namespace
172
    * @return Localization_Locale[]
173
    */
174
    public static function getLocalesByNS(string $namespace)
175
    {
176
        if(isset(self::$locales[$namespace])) {
177
            return array_values(self::$locales[$namespace]);
178
        }
179
        
180
        throw new Localization_Exception(
181
            'No locales available in namespace',
182
            sprintf(
183
                'The namespace [%s] does not exist.',
184
                $namespace
185
            ),
186
            self::ERROR_NO_LOCALES_IN_NAMESPACE
187
        );
188
    }
189
    
190
   /**
191
    * Adds an application locale to use in the application.
192
    * 
193
    * @param string $localeName
194
    * @return Localization_Locale
195
    */
196
    public static function addAppLocale(string $localeName) : Localization_Locale
197
    {
198
        return self::addLocaleByNS($localeName, self::NAMESPACE_APPLICATION);
199
    }
200
    
201
   /**
202
    * Adds a content locale to use for content in the application.
203
    * 
204
    * @param string $localeName
205
    * @return Localization_Locale
206
    */
207
    public static function addContentLocale(string $localeName) : Localization_Locale
208
    {
209
        return self::addLocaleByNS($localeName, self::NAMESPACE_CONTENT);
210
    }
211
    
212
   /**
213
    * Adds a locale to the specified namespace.
214
    * 
215
    * @param string $localeName
216
    * @param string $namespace
217
    * @return Localization_Locale
218
    */
219
    public static function addLocaleByNS(string $localeName, string $namespace) : Localization_Locale
220
    {
221
        if(!isset(self::$locales[$namespace])) {
222
            self::$locales[$namespace] = array();
223
        }
224
        
225
        if(!isset(self::$locales[$namespace][$localeName])) 
226
        {
227
            self::$locales[$namespace][$localeName] = self::createLocale($localeName);
228
            
229
            // sort the locales on add: less resource intensive
230
            // than doing it on getting locales.
231
            uasort(self::$locales[$namespace], function(Localization_Locale $a, Localization_Locale $b) {
232
                return strnatcasecmp($a->getLabel(), $b->getLabel());
233
            });
234
        }
235
        
236
        return self::$locales[$namespace][$localeName];
237
    }
238
239
    /**
240
     * @param string $localeName
241
     * @return Localization_Locale
242
     *
243
     * @throws Localization_Exception
244
     * @see Localization::ERROR_LOCALE_NOT_FOUND
245
     */
246
    protected static function createLocale(string $localeName) : Localization_Locale
247
    {
248
        $class = '\AppLocalize\Locale\\'.$localeName;
249
250
        try
251
        {
252
            $locale = new $class();
253
254
            if ($locale instanceof Localization_Locale)
255
            {
256
                return $locale;
257
            }
258
        }
259
        catch (Throwable $e)
260
        {
261
262
        }
263
264
        throw new Localization_Exception(
265
            'Locale not supported.',
266
            sprintf(
267
                'The locale class [%s] does not exist.',
268
                $localeName
269
            ),
270
            self::ERROR_LOCALE_NOT_FOUND
271
        );
272
    }
273
274
    private static ?CountryCollection $countries = null;
275
276
    /**
277
     * Retrieves the country collection instance to
278
     * access all available countries.
279
     *
280
     * @return CountryCollection
281
     */
282
    public static function createCountries() : CountryCollection
283
    {
284
        if(!isset(self::$countries)) {
285
            self::$countries = CountryCollection::getInstance();
286
        }
287
288
        return self::$countries;
289
    }
290
291
    private static ?CurrencyCollection $currencies = null;
292
293
    /**
294
     * Retrieves the currency collection instance to
295
     * access all available currencies.
296
     *
297
     * @return CurrencyCollection
298
     */
299
    public static function createCurrencies() : CurrencyCollection
300
    {
301
        if(!isset(self::$currencies)) {
302
            self::$currencies = CurrencyCollection::getInstance();
303
        }
304
305
        return self::$currencies;
306
    }
307
308
    /**
309
     * Creates a new country object for the specified country, e.g. "uk".
310
     *
311
     * @param string $id
312
     * @return BaseCountry
313
     *
314
     * @deprecated Use the collection instead: {@see self::createCountries()}
315
     */
316
    public static function createCountry(string $id) : BaseCountry
317
    {
318
        return self::createCountries()->getByID($id);
319
    }
320
321
    /**
322
     * Retrieves the currency of the selected app locale.
323
     *
324
     * @return CurrencyInterface
325
     *
326
     * @throws Localization_Exception
327
     * @see Localization::ERROR_NO_LOCALE_SELECTED_IN_NS
328
     */
329
    public static function getAppCurrency() : CurrencyInterface
330
    {
331
        return self::getCurrencyNS(self::NAMESPACE_APPLICATION);
332
    }
333
334
    /**
335
     * Retrieves the currency of the selected content locale.
336
     *
337
     * @return CurrencyInterface
338
     *
339
     * @throws Localization_Exception
340
     * @see Localization::ERROR_NO_LOCALE_SELECTED_IN_NS
341
     */
342
    public static function getContentCurrency() : CurrencyInterface
343
    {
344
        return self::getCurrencyNS(self::NAMESPACE_CONTENT);
345
    }
346
347
    /**
348
     * Retrieves the currency of the selected locale in the specified namespace.
349
     *
350
     * @param string $namespace
351
     * @return CurrencyInterface
352
     *
353
     * @throws Localization_Exception
354
     * @see Localization::ERROR_NO_LOCALE_SELECTED_IN_NS
355
     */
356
    public static function getCurrencyNS(string $namespace) : CurrencyInterface
357
    {
358
        return self::getSelectedLocaleByNS($namespace)->getCurrency();
359
    }
360
    
361
    /**
362
     * Retrieves the selected application locale instance. 
363
     *
364
     * @return Localization_Locale
365
     */
366
    public static function getAppLocale() : Localization_Locale
367
    {
368
        return self::getSelectedLocaleByNS(self::NAMESPACE_APPLICATION);
369
    }
370
    
371
   /**
372
    * Retrieves the name of the selected application locale.
373
    * 
374
    * @return string
375
    */
376
    public static function getAppLocaleName() : string
377
    {
378
        return self::getLocaleNameByNS(self::NAMESPACE_APPLICATION);
379
    }
380
    
381
   /**
382
    * Retrieves the names of the available application locales.
383
    * @return string[]
384
    */
385
    public static function getAppLocaleNames() : array
386
    {
387
        return self::getLocaleNamesByNS(self::NAMESPACE_APPLICATION);
388
    }
389
    
390
   /**
391
    * Retrieves the selected locale name in the specified namespace.
392
    * 
393
    * @param string $namespace
394
    * @throws Localization_Exception
395
    * @return string
396
    */
397
    public static function getLocaleNameByNS(string $namespace) : string
398
    {
399
        return self::getSelectedLocaleByNS($namespace)->getName();
400
    }
401
    
402
   /**
403
    * Retrieves the selected locale instance for the specified namespace.
404
    * 
405
    * @param string $namespace
406
    * @return Localization_Locale
407
    * @throws Localization_Exception
408
    * @see Localization::ERROR_NO_LOCALE_SELECTED_IN_NS
409
    */
410
    public static function getSelectedLocaleByNS(string $namespace) : Localization_Locale
411
    {
412
        self::requireNamespace($namespace);
413
        
414
        if(isset(self::$selected[$namespace])) {
415
            return self::$selected[$namespace];
416
        }
417
        
418
        throw new Localization_Exception(
419
            'No selected locale in namespace.',
420
            sprintf(
421
                'Cannot retrieve selected locale: no locale has been selected in the namespace [%s].',
422
                $namespace
423
            ),
424
            self::ERROR_NO_LOCALE_SELECTED_IN_NS
425
        );
426
    }
427
    
428
   /**
429
    * Stores the selected locale names by namespace.
430
    * @var array<string,Localization_Locale>
431
    */
432
    protected static $selected = array();
433
434
   /**
435
    * Selects the active locale for the specified namespace.
436
    *
437
    * NOTE: Triggers the "LocaleChanged" event.
438
    * 
439
    * @param string $localeName
440
    * @param string $namespace
441
    * @return Localization_Locale
442
    * @throws Localization_Exception
443
    *
444
    * @see LocaleChanged
445
    */
446
    public static function selectLocaleByNS(string $localeName, string $namespace) : Localization_Locale
447
    {
448
        self::requireNamespace($namespace);
449
        
450
        $locale = self::addLocaleByNS($localeName, $namespace);
451
        $previous = null;
452
        
453
        if(isset(self::$selected[$namespace])) 
454
        {
455
            if(self::$selected[$namespace]->getName() === $localeName) {
456
                return $locale;
457
            }
458
            
459
            $previous = self::$selected[$namespace];
460
        }
461
        
462
        self::$translator = null;
463
464
        self::$selected[$namespace] = $locale;
465
        
466
        self::triggerEvent(
467
            self::EVENT_LOCALE_CHANGED,
468
            array(
469
                $namespace,
470
                $previous, 
471
                self::$selected[$namespace]
472
            )
473
        );
474
        
475
        return $locale;
476
    }
477
478
    /**
479
     * Triggers the specified event, with the provided arguments.
480
     *
481
     * @param string $name The event name.
482
     * @param array $argsList
483
     * @return Localization_Event
484
     * @see Localization_Event
485
     */
486
    protected static function triggerEvent(string $name, array $argsList) : Localization_Event
487
    {
488
        $class = self::resolveEventClass($name);
489
490
        $event = ClassHelper::requireObjectInstanceOf(
491
            Localization_Event::class,
492
            new $class($argsList)
493
        );
494
        
495
        if(!isset(self::$listeners[$name])) {
496
            return $event;
497
        }
498
        
499
        foreach(self::$listeners[$name] as $listener) 
500
        {
501
            $callArgs = $listener['args'];
502
            array_unshift($callArgs, $event);
503
            
504
            call_user_func_array($listener['callback'], $callArgs);
505
        }
506
        
507
        return $event;
508
    }
509
510
    /**
511
     * Adds a listener to the specified event name.
512
     *
513
     * @param string $eventName
514
     * @param callable $callback
515
     * @param array $args Additional arguments to add to the event
516
     * @return int The listener number.
517
     *
518
     * @throws Localization_Exception
519
     * @see Localization::ERROR_UNKNOWN_EVENT_NAME
520
     */
521
    public static function addEventListener(string $eventName, $callback, array $args=array()) : int
522
    {
523
        if(!isset(self::$listeners[$eventName])) {
524
            self::$listeners[$eventName] = array();
525
        }
526
        
527
        self::$listenersCounter++;
528
        
529
        self::$listeners[$eventName][] = array(
530
            'class' => self::resolveEventClass($eventName),
531
            'callback' => $callback,
532
            'args' => $args,
533
            'id' => self::$listenersCounter
534
        );
535
        
536
        return self::$listenersCounter;
537
    }
538
539
    private static function resolveEventClass(string $eventName) : string
540
    {
541
        $className = ClassHelper::resolveClassName(
542
            Localization_Event::class.'_'.$eventName,
543
            'AppLocalize'
544
        );
545
546
        if(class_exists($className)) {
547
            return $className;
548
        }
549
550
        throw new Localization_Exception(
551
            sprintf('Unknown localization event [%s].', $eventName),
552
            sprintf('The required event class [%s] is not present.', $className),
553
            self::ERROR_UNKNOWN_EVENT_NAME
554
        );
555
    }
556
557
    /**
558
     * Adds an event listener for the <code>LocaleChanged</code> event,
559
     * which is triggered every time a locale is changed in any of the
560
     * available namespaces.
561
     *
562
     * The first parameter of the callback is always the event instance.
563
     *
564
     * @param callable $callback The listener function to call.
565
     * @param array $args Optional indexed array with additional arguments to pass on to the callback function.
566
     * @return int
567
     * @throws Localization_Exception
568
     * @see LocaleChanged
569
     */
570
    public static function onLocaleChanged($callback, array $args=array()) : int
571
    {
572
        return self::addEventListener(self::EVENT_LOCALE_CHANGED, $callback, $args);
573
    }
574
575
    public static function onClientFolderChanged(callable $callback, array $args=array()) : int
576
    {
577
        return self::addEventListener(self::EVENT_CLIENT_FOLDER_CHANGED, $callback, $args);
578
    }
579
580
    public static function onCacheKeyChanged(callable $callback, array $args=array()) : int
581
    {
582
        return self::addEventListener(self::EVENT_CACHE_KEY_CHANGED, $callback, $args);
583
    }
584
585
    /**
586
     * Selects the application locale to use.
587
     *
588
     * @param string $localeName
589
     * @return Localization_Locale
590
     * @throws Localization_Exception
591
     */
592
    public static function selectAppLocale(string $localeName) : Localization_Locale
593
    {
594
        return self::selectLocaleByNS($localeName, self::NAMESPACE_APPLICATION);
595
    }
596
597
   /**
598
    * Retrieves an application locale by its name. 
599
    * Note that the locale must have been added first.
600
    * 
601
    * @param string $localeName
602
    * @throws Localization_Exception
603
    * @return Localization_Locale
604
    * @see Localization::appLocaleExists()
605
    */
606
    public static function getAppLocaleByName(string $localeName) : Localization_Locale
607
    {
608
        return self::getLocaleByNameNS($localeName, self::NAMESPACE_APPLICATION);
609
    }
610
611
    /**
612
     * Checks by the locale name if the specified locale is
613
     * available as a locale for the application.
614
     *
615
     * @param string $localeName
616
     * @return boolean
617
     */
618
    public static function appLocaleExists(string $localeName) : bool
619
    {
620
        return self::localeExistsInNS($localeName, self::NAMESPACE_APPLICATION);
621
    }
622
   
623
    public static function localeExistsInNS(string $localeName, string $namespace) : bool
624
    {
625
        return isset(self::$locales[$namespace]) && isset(self::$locales[$namespace][$localeName]);
626
    }
627
628
    /**
629
     * Retrieves an indexed array with all available content locales,
630
     * sorted by locale label.
631
     *
632
     * @return Localization_Locale[];
633
     * @throws Localization_Exception
634
     */
635
    public static function getContentLocales()
636
    {
637
        return self::getLocalesByNS(self::NAMESPACE_CONTENT);
638
    }
639
    
640
   /**
641
    * Retrieves the names of all content locales that have been added.
642
    * @return string[]
643
    */
644
    public static function getContentLocaleNames()
645
    {
646
        return self::getLocaleNamesByNS(self::NAMESPACE_CONTENT);
647
    }
648
649
    /**
650
     * Retrieves all locale names available in the specified namespace.
651
     * The names are sorted alphabetically.
652
     *
653
     * @param string $namespace
654
     * @return string[]
655
     * @throws Localization_Exception
656
     */
657
    public static function getLocaleNamesByNS(string $namespace) : array
658
    {
659
        self::requireNamespace($namespace);
660
        
661
        $names = array_keys(self::$locales[$namespace]);
662
        
663
        sort($names);
664
        
665
        return $names;
666
     }
667
    
668
    /**
669
     * Checks by the locale name if the specified locale is
670
     * available as a locale for the user data.
671
     *
672
     * @param string $localeName
673
     * @return boolean
674
     */
675
    public static function contentLocaleExists(string $localeName) : bool
676
    {
677
        return self::localeExistsInNS($localeName, self::NAMESPACE_CONTENT);
678
    }
679
680
    /**
681
     * Retrieves a specific content locale object by the locale name.
682
     * Note that you should check if it exists first to avoid triggering
683
     * an Exception if it does not.
684
     *
685
     * @param string $localeName
686
     * @throws Localization_Exception
687
     * @return Localization_Locale
688
     * @see Localization::contentLocaleExists()
689
     */
690
    public static function getContentLocaleByName($localeName) : Localization_Locale
691
    {
692
        return self::getLocaleByNameNS($localeName, self::NAMESPACE_CONTENT);
693
    }
694
    
695
   /**
696
    * Retrieves a locale by its name in the specified namespace.
697
    * 
698
    * @param string $localeName
699
    * @param string $namespace
700
    * @throws Localization_Exception
701
    * @return Localization_Locale
702
    */
703
    public static function getLocaleByNameNS(string $localeName, string $namespace) : Localization_Locale
704
    {
705
        self::requireNamespace($namespace);
706
        
707
        if(isset(self::$locales[$namespace]) && isset(self::$locales[$namespace][$localeName])) {
708
            return self::$locales[$namespace][$localeName];
709
        }
710
        
711
        throw new Localization_Exception(
712
            'Unknown locale in namespace',
713
            sprintf(
714
                'The locale [%s] has not been added to the namespace [%s].',
715
                $localeName,
716
                $namespace
717
            ),
718
            self::ERROR_UNKNOWN_LOCALE_IN_NS
719
        );
720
    }
721
722
    /**
723
     * Retrieves the currently selected content locale.
724
     *
725
     * @return Localization_Locale
726
     * @throws Localization_Exception
727
     */
728
    public static function getContentLocale() : Localization_Locale
729
    {
730
        return self::getSelectedLocaleByNS(self::NAMESPACE_CONTENT);
731
    }
732
733
    /**
734
     * @return string
735
     * @throws Localization_Exception
736
     */
737
    public static function getContentLocaleName() : string
738
    {
739
        return self::getSelectedLocaleByNS(self::NAMESPACE_CONTENT)->getName();
740
    }
741
742
    /**
743
     * @param Localization_Locale $locale
744
     * @return bool
745
     */
746
    public static function isActiveAppLocale(Localization_Locale $locale) : bool
747
    {
748
        return $locale->getName() === self::getAppLocaleName();
749
    }
750
751
    /**
752
     * Checks whether the specified locale is the current content locale.
753
     * @param Localization_Locale $locale
754
     * @return boolean
755
     * @throws Localization_Exception
756
     */
757
    public static function isActiveContentLocale(Localization_Locale $locale) : bool
758
    {
759
        return $locale->getName() === self::getContentLocaleName();
760
    }
761
762
    /**
763
     * Selects a specific content locale
764
     * @param string $localeName
765
     * @return Localization_Locale
766
     * @throws Localization_Exception
767
     */
768
    public static function selectContentLocale(string $localeName) : Localization_Locale
769
    {
770
        return self::selectLocaleByNS($localeName, self::NAMESPACE_CONTENT);
771
    }
772
    
773
   /**
774
    * Checks whether the localization has been configured entirely.
775
    * @return bool
776
    */
777
    public static function isConfigured() : bool
778
    {
779
        return self::$configured;
780
    }
781
782
    /**
783
     * @param Localization_Locale|null $locale
784
     * @return Localization_Translator
785
     */
786
    public static function getTranslator(?Localization_Locale $locale=null) : Localization_Translator
787
    {
788
        if($locale !== null)
789
        {
790
            $obj = new Localization_Translator();
791
            $obj->addSources(self::getSources());
792
            $obj->setTargetLocale($locale);
793
            return $obj;
794
        }
795
            
796
        if(!isset(self::$translator)) 
797
        {
798
            $obj = new Localization_Translator();
799
            $obj->addSources(self::getSources());
800
            $obj->setTargetLocale(self::getAppLocale());
801
            self::$translator = $obj;
802
        }
803
804
        return self::$translator;
805
    }
806
807
    public static function countContentLocales() : int
808
    {
809
        return self::countLocalesByNS(self::NAMESPACE_CONTENT);
810
    }
811
812
    public static function countAppLocales() : int
813
    {
814
        return self::countLocalesByNS(self::NAMESPACE_APPLICATION);
815
    }
816
    
817
    public static function countLocalesByNS(string $namespace) : int
818
    {
819
        self::requireNamespace($namespace);
820
        
821
        if(isset(self::$locales[$namespace])) {
822
            return count(self::$locales[$namespace]);
823
        }
824
        
825
        return 0;
826
    }
827
828
    /**
829
     * @param string $namespace
830
     * @throws Localization_Exception
831
     */
832
    protected static function requireNamespace(string $namespace) : void
833
    {
834
        if(isset(self::$locales[$namespace])) {
835
            return;
836
        }
837
        
838
        throw new Localization_Exception(
839
            'Cannot count locales in unknown namespace',
840
            sprintf(
841
                'The namespace [%s] does not exist.',
842
                $namespace
843
            ),
844
            self::ERROR_UNKNOWN_NAMESPACE
845
        );
846
    }
847
    
848
   /**
849
    * Injects a content locales selector element into the specified
850
    * HTML QuickForm2 container.
851
    * 
852
    * @param string $elementName
853
    * @param HTML_QuickForm2_Container $container
854
    * @param string $label
855
    * @return HTML_QuickForm2_Element_Select
856
    */
857
    public static function injectContentLocalesSelector(string $elementName, HTML_QuickForm2_Container $container, string $label='') : HTML_QuickForm2_Element_Select
858
    {
859
        return self::injectLocalesSelectorNS($elementName, self::NAMESPACE_CONTENT, $container, $label);
860
    }
861
    
862
   /**
863
    * Injects an app locales selector element into the specified
864
     * HTML QuickForm2 container.
865
     * 
866
    * @param string $elementName
867
    * @param HTML_QuickForm2_Container $container
868
    * @param string $label
869
    * @return HTML_QuickForm2_Element_Select
870
    */
871
    public static function injectAppLocalesSelector(string $elementName, HTML_QuickForm2_Container $container, string $label='') : HTML_QuickForm2_Element_Select
872
    {
873
        return self::injectLocalesSelectorNS($elementName, self::NAMESPACE_APPLICATION, $container, $label);
874
    }
875
876
    /**
877
     * Injects a locales selector element into the specified
878
     * HTML QuickForm2 container, for the specified locales
879
     * namespace.
880
     *
881
     * @param string $elementName
882
     * @param string $namespace
883
     * @param HTML_QuickForm2_Container $container
884
     * @param string $label
885
     * @return HTML_QuickForm2_Element_Select
886
     * @throws Localization_Exception
887
     */
888
    public static function injectLocalesSelectorNS(string $elementName, string $namespace, HTML_QuickForm2_Container $container, string $label='') : HTML_QuickForm2_Element_Select
889
    {
890
        if(empty($label)) {
891
            $label = t('Language');
892
        }
893
894
        $select = $container->addSelect($elementName);
895
        $select->setLabel($label);
896
897
        $locales = self::getLocalesByNS($namespace);
898
        
899
        foreach($locales as $locale) {
900
            $select->addOption($locale->getLabel(), $locale->getName());
901
        }
902
903
        return $select;
904
    }
905
906
   /**
907
    * @var Localization_Source[]
908
    */
909
    protected static $sources = array();
910
    
911
   /**
912
    * @var string[]
913
    */
914
    protected static $excludeFolders = array();
915
    
916
   /**
917
    * @var string[]
918
    */
919
    protected static $excludeFiles = array();
920
    
921
   /**
922
    * Retrieves all currently available sources.
923
    * 
924
    * @return Localization_Source[]
925
    */
926
    public static function getSources() : array
927
    {
928
        return self::$sources;
929
    }
930
    
931
    public static function addExcludeFolder(string $folderName) : void
932
    { 
933
        if(!in_array($folderName, self::$excludeFolders)) {
934
            self::$excludeFolders[] = $folderName;
935
        }
936
    }
937
    
938
    public static function addExcludeFile(string $fileName) : void
939
    {
940
        if(!in_array($fileName, self::$excludeFiles)) {
941
            self::$excludeFiles[] = $fileName;
942
        }
943
    }
944
    
945
    public static function addSourceFolder(string $alias, string $label, string $group, string $storageFolder, string $path) : Localization_Source_Folder
946
    {
947
        $source = new Localization_Source_Folder($alias, $label, $group, $storageFolder, $path);
948
        self::$sources[] = $source;
949
950
        usort(self::$sources, function(Localization_Source $a, Localization_Source $b) {
951
            return strnatcasecmp($a->getLabel(), $b->getLabel());
952
        });
953
        
954
        return $source;
955
    }
956
    
957
   /**
958
    * Retrieves all sources grouped by their group name.
959
    * @return array
960
    */
961
    public static function getSourcesGrouped()
962
    {
963
        $sources = self::getSources();
964
        
965
        $grouped = array();
966
        
967
        foreach($sources as $source) 
968
        {
969
            $group = $source->getGroup();
970
            
971
            if(!isset($grouped[$group])) {
972
                $grouped[$group] = array();
973
            }
974
            
975
            $grouped[$group][] = $source;
976
        }
977
        
978
        return $grouped;
979
    }
980
    
981
   /**
982
    * Checks whether a specific source exists by its ID.
983
    * @param string $sourceID
984
    * @return boolean
985
    */
986
    public static function sourceExists(string $sourceID) : bool
987
    {
988
        $sources = self::getSources();
989
        foreach($sources as $source) {
990
            if($source->getID() == $sourceID) {
991
                return true;
992
            }
993
        }
994
        
995
        return false;
996
    }
997
    
998
   /**
999
    * Checks whether a specific source exists by its alias.
1000
    * @param string $sourceAlias
1001
    * @return boolean
1002
    */
1003
    public static function sourceAliasExists(string $sourceAlias) : bool
1004
    {
1005
        $sources = self::getSources();
1006
        foreach($sources as $source) {
1007
            if($source->getAlias() == $sourceAlias) {
1008
                return true;
1009
            }
1010
        }
1011
        
1012
        return false;
1013
    }
1014
1015
   /**
1016
    * Retrieves a localization source by its ID.
1017
    * 
1018
    * @param string $sourceID
1019
    * @throws Localization_Exception
1020
    * @return Localization_Source
1021
    */
1022
    public static function getSourceByID(string $sourceID) : Localization_Source
1023
    {
1024
        $sources = self::getSources();
1025
        foreach($sources as $source) {
1026
            if($source->getID() == $sourceID) {
1027
                return $source;
1028
            }
1029
        }
1030
        
1031
        throw new Localization_Exception(
1032
            'Unknown localization source',
1033
            sprintf(
1034
                'The source [%s] has not been added. Available soources are: [%s].',
1035
                $sourceID,
1036
                implode(', ', self::getSourceIDs())
1037
            )
1038
        );
1039
    }
1040
    
1041
    /**
1042
     * Retrieves a localization source by its ID.
1043
     *
1044
     * @param string $sourceAlias
1045
     * @throws Localization_Exception
1046
     * @return Localization_Source
1047
     */
1048
    public static function getSourceByAlias(string $sourceAlias) : Localization_Source
1049
    {
1050
        $sources = self::getSources();
1051
        foreach($sources as $source) {
1052
            if($source->getAlias() == $sourceAlias) {
1053
                return $source;
1054
            }
1055
        }
1056
        
1057
        throw new Localization_Exception(
1058
            'Unknown localization source',
1059
            sprintf(
1060
                'The source [%s] has not been added. Available soources are: [%s].',
1061
                $sourceAlias,
1062
                implode(', ', self::getSourceAliases())
1063
            )
1064
        );
1065
    }
1066
1067
    /**
1068
     * Creates the scanner instance that is used to find
1069
     * all translatable strings in the application.
1070
     *
1071
     * @return Localization_Scanner
1072
     * @throws Localization_Exception
1073
     */
1074
    public static function createScanner() : Localization_Scanner
1075
    {
1076
        self::requireConfiguration();
1077
        
1078
        return new Localization_Scanner(self::$storageFile);
1079
    }
1080
    
1081
    public static function log(string $message) : void
0 ignored issues
show
The parameter $message is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1081
    public static function log(/** @scrutinizer ignore-unused */ string $message) : void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1082
    {
1083
        // FIXME: TODO: Add this
1084
    }
1085
1086
    /**
1087
     * Configures the localization for the application:
1088
     * sets the location of the required files and folders.
1089
     * Also updated the client library files as needed.
1090
     *
1091
     * @param string $storageFile Where to store the file analysis storage file.
1092
     * @param string $clientLibrariesFolder Where to put the client libraries and translation files. Will be created if it does not exist. Optional: if not set, client libraries will not be created.
1093
     * @throws FileHelper_Exception
1094
     * @throws Localization_Exception
1095
     */
1096
    public static function configure(string $storageFile, string $clientLibrariesFolder='') : void
1097
    {
1098
        self::$configured = true;
1099
        
1100
        self::$storageFile = $storageFile;
1101
        self::$clientFolder = $clientLibrariesFolder;
1102
1103
        // only write the client libraries to disk if the folder
1104
        // has been specified.
1105
        if(!empty($clientLibrariesFolder)) 
1106
        {
1107
            self::writeClientFiles();
1108
        }
1109
    }
1110
    
1111
   /**
1112
    * Sets a key that is used to verify whether the client
1113
    * libraries have to be refreshed. A common use is to set
1114
    * this to the application's version number to guarantee
1115
    * new texts are automatically used with each release.
1116
    * 
1117
    * NOTE: Otherwise files are refreshed only when saving 
1118
    * them in the editor UI.
1119
    *  
1120
    * @param string $key
1121
    */
1122
    public static function setClientLibrariesCacheKey(string $key) : void
1123
    {
1124
        if($key !== self::$clientCacheKey) {
1125
            self::$clientCacheKey = $key;
1126
            self::triggerEvent(self::EVENT_CACHE_KEY_CHANGED, array($key));
1127
        }
1128
    }
1129
    
1130
    public static function getClientLibrariesCacheKey() : string
1131
    {
1132
        return self::$clientCacheKey;
1133
    }
1134
    
1135
   /**
1136
    * Sets the folder where client libraries are to be stored.
1137
    * @param string $folder
1138
    */
1139
    public static function setClientLibrariesFolder(string $folder) : void
1140
    {
1141
        if($folder !== self::$clientFolder) {
1142
            self::$clientFolder = $folder;
1143
            self::triggerEvent(self::EVENT_CLIENT_FOLDER_CHANGED, array($folder));
1144
        }
1145
    }
1146
    
1147
   /**
1148
    * Retrieves the path to the folder in which the client
1149
    * libraries should be stored.
1150
    * 
1151
    * NOTE: Can return an empty string, when this is disabled.
1152
    * 
1153
    * @return string
1154
    */
1155
    public static function getClientLibrariesFolder() : string
1156
    {
1157
        return self::$clientFolder;
1158
    }
1159
1160
    /**
1161
     * Writes / updates the client library files on disk,
1162
     * at the location specified in the {@link Localization::configure()}
1163
     * method.
1164
     *
1165
     * @param bool $force Whether to refresh the files, even if they exist.
1166
     * @throws Localization_Exception|FileHelper_Exception
1167
     * @see Localization_ClientGenerator
1168
     */
1169
    public static function writeClientFiles(bool $force=false) : void
0 ignored issues
show
The parameter $force is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1169
    public static function writeClientFiles(/** @scrutinizer ignore-unused */ bool $force=false) : void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1170
    {
1171
        self::createGenerator()->writeFiles();
1172
    }
1173
1174
    private static ?Localization_ClientGenerator $generator = null;
1175
1176
   /**
1177
    * Creates a new instance of the client generator class
1178
    * that is used to write the localization files into the
1179
    * target folder on disk.
1180
    * 
1181
    * @return Localization_ClientGenerator
1182
    */
1183
    public static function createGenerator() : Localization_ClientGenerator
1184
    {
1185
        if(!isset(self::$generator)) {
1186
            self::$generator = new Localization_ClientGenerator();
1187
        }
1188
1189
        return self::$generator;
1190
    }
1191
1192
    /**
1193
     * @return string
1194
     * @throws Localization_Exception
1195
     */
1196
    public static function getClientFolder() : string
1197
    {
1198
        self::requireConfiguration();
1199
        
1200
        return self::$clientFolder;
1201
    }
1202
1203
    /**
1204
     * @throws Localization_Exception
1205
     */
1206
    protected static function requireConfiguration() : void
1207
    {
1208
        if(!self::$configured) 
1209
        {
1210
            throw new Localization_Exception(
1211
                'The localization configuration is incomplete.',
1212
                'The configure method has not been called.',
1213
                self::ERROR_CONFIGURE_NOT_CALLED
1214
            );
1215
        }
1216
1217
        if(empty(self::$storageFile))
1218
        {
1219
            throw new Localization_Exception(
1220
                'No localization storage file set',
1221
                'To use the scanner, the storage file has to be set using the setStorageFile method.',
1222
                self::ERROR_NO_STORAGE_FILE_SET
1223
            );
1224
        }
1225
        
1226
        if(empty(self::$sources)) 
1227
        {
1228
            throw new Localization_Exception(
1229
                'No source folders have been defined.',
1230
                'At least one source folder has to be configured using the addSourceFolder method.',
1231
                self::ERROR_NO_SOURCES_ADDED
1232
            );
1233
        }
1234
    }
1235
1236
    /**
1237
     * Creates the editor instance that can be used to
1238
     * display the localization UI to edit translatable
1239
     * strings in the browser.
1240
     *
1241
     * @return Localization_Editor
1242
     * @throws Localization_Exception
1243
     */
1244
    public static function createEditor() : Localization_Editor
1245
    {
1246
        self::requireConfiguration();
1247
        
1248
        return new Localization_Editor();
1249
    }
1250
    
1251
   /**
1252
    * Retrieves a list of all available source IDs.
1253
    * @return string[]
1254
    */
1255
    public static function getSourceIDs() : array
1256
    {
1257
        $ids = array();
1258
        
1259
        foreach(self::$sources as $source) {
1260
            $ids[] = $source->getID();
1261
        }
1262
        
1263
        return $ids;
1264
    }
1265
    
1266
    /**
1267
     * Retrieves a list of all available source aliases.
1268
     * @return string[]
1269
     */
1270
    public static function getSourceAliases() : array
1271
    {
1272
        $aliases = array();
1273
        
1274
        foreach(self::$sources as $source) {
1275
            $aliases[] = $source->getAlias();
1276
        }
1277
        
1278
        return $aliases;
1279
    }
1280
    
1281
   /**
1282
    * Resets all locales to the built-in locale.
1283
    */
1284
    public static function reset() : void
1285
    {
1286
        self::$locales = array();
1287
        self::$selected = array();
1288
1289
        self::addAppLocale(self::BUILTIN_LOCALE_NAME);
1290
        self::addContentLocale(self::BUILTIN_LOCALE_NAME);
1291
        
1292
        self::selectAppLocale(self::BUILTIN_LOCALE_NAME);
1293
        self::selectContentLocale(self::BUILTIN_LOCALE_NAME);
1294
    }
1295
    
1296
    /**
1297
     * Indexed array with locale names supported by the application
1298
     * @var string[]
1299
     */
1300
    protected static $supportedLocales = array();
1301
1302
    /**
1303
     * Retrieves a list of all supported locales.
1304
     *
1305
     * @return string[]
1306
     * @throws FileHelper_Exception
1307
     */
1308
    public static function getSupportedLocaleNames() : array
1309
    {
1310
        if(empty(self::$supportedLocales))
1311
        {
1312
            self::$supportedLocales = FileHelper::createFileFinder(__DIR__.'/Localization/Locale')
1313
                ->getPHPClassNames();
1314
        }
1315
1316
        return self::$supportedLocales;
1317
    }
1318
    
1319
   /**
1320
    * Checks whether the specified locale is supported.
1321
    * 
1322
    * @param string $localeName
1323
    * @return bool
1324
    */
1325
    public static function isLocaleSupported(string $localeName) : bool
1326
    {
1327
        return file_exists(__DIR__.'/Localization/Locale/'.$localeName.'.php');
1328
    }
1329
1330
    private static ?string $version = null;
1331
1332
    public static function getVersion() : string
1333
    {
1334
        if(isset(self::$version)) {
1335
            return self::$version;
1336
        }
1337
1338
        $versionFile = FileInfo::factory(__DIR__.'/../version.txt');
1339
1340
        if($versionFile->exists()) {
1341
            self::$version = $versionFile->getContents();
1342
            return self::$version;
1343
        }
1344
1345
        self::$version = ChangelogParser::parseMarkdownFile(__DIR__.'/../changelog.md')
1346
            ->requireLatestVersion()
1347
            ->getVersionInfo()
1348
            ->getTagVersion();
1349
1350
        $versionFile->putContents(self::$version);
1351
1352
        return self::$version;
1353
    }
1354
}
1355
1356
Localization::init();
1357