1#!/usr/bin/env php
2<?php
3
4function handleError($errno, $errstr, $errfile, $errline)
5{
6 throw new Exception("{$errstr} in {$errfile} @ line {$errline}", $errno);
7}
8set_error_handler('handleError');
9
10try {
11 if (isset($argv) && is_array($argv) && count($argv) > 1) {
12 $optionArray = array_values($argv);
13 array_shift($optionArray);
14 } else {
15 $optionArray = array();
16 }
17 $options = Options::fromArray($optionArray);
18 echo 'Source location : ', $options->getSourceLocation(), "\n";
19 echo 'CLDR version : ', $options->getCLDRVersion() === null ? 'latest available' : $options->getCLDRVersion(), "\n";
20 echo 'Format version : ', Options::FORMAT_VERSION, "\n";
21 echo 'Locales : ', $options->describeLocales(), "\n";
22 echo 'Output directory: ', str_replace('/', DIRECTORY_SEPARATOR, $options->getOutputDirectory()), "\n";
23 $fileUtils = new FileUtils();
24 $courier = new Courier($options);
25 $sourceData = new SourceData($fileUtils, $courier);
26 echo 'Fetching data state... ';
27 $sourceData->readState();
28 echo "done.\n";
29 $cldrVersion = $options->getCLDRVersion();
30 if ($cldrVersion === null) {
31 echo 'Determining the latest CLDR version... ';
32 $cldrVersion = $sourceData->getLatestCLDRVersion();
33 $sourceData->setCLDRVersion($cldrVersion);
34 echo $cldrVersion, "\n";
35 } else {
36 echo 'Checking CLDR version... ';
37 $sourceData->setCLDRVersion($cldrVersion);
38 echo "done.\n";
39 }
40 $locales = $options->finalizeLocalesList($sourceData->getAvailableLocales());
41 if (empty($locales)) {
42 throw new UserMessageException('No locale to be fetched.');
43 }
44 if (is_dir($options->getOutputDirectory()) && $options->getResetPunicData()) {
45 echo 'Clearing current Punic data... ';
46 $fileUtils->deleteFromFilesystem($options->getOutputDirectory(), true);
47 echo "done.\n";
48 }
49 echo "Fetching language files:\n";
50 foreach ($locales as $localeID) {
51 echo " - {$localeID}...";
52 $destDir = $options->getOutputDirectory().'/'.str_replace('_', '-', $localeID);
53 if (is_dir($destDir)) {
54 echo "destination directory exists - SKIPPED.\n";
55 } else {
56 $sourceData->fetchLocale($localeID, $destDir);
57 echo "done.\n";
58 }
59 }
60 echo "Fetching supplemental files:\n";
61 foreach ($sourceData->getSupplementalFiles() as $supplementalFile) {
62 echo " - {$supplementalFile}...";
63 $destFile = $options->getOutputDirectory().'/'.$supplementalFile;
64 if (is_file($destFile)) {
65 echo "destination file exists - SKIPPED.\n";
66 } else {
67 $sourceData->fetchSupplementalFile($supplementalFile, $destFile);
68 echo "done.\n";
69 }
70 }
71 if ($options->shouldUpdateTestFiles()) {
72 echo "Updating Punic test files:\n";
73 $testDirectory = $options->getPunicTestsDirectory();
74 if (!is_dir($testDirectory)) {
75 echo "Punic test directory not found - SKIPPED.\n";
76 } else {
77 $testFiles = $sourceData->getTestFiles();
78 if (count($testFiles) === 0) {
79 echo "No test files available - SKIPPED.\n";
80 } else {
81 foreach ($sourceData->getTestFiles() as $testFile) {
82 echo " - {$testFile['destFile']}...";
83 $testFilePath = $testDirectory;
84 if(isset($testFile['destDir']) && $testFile['destDir'] !== '') {
85 $testFilePath .= '/' . $testFile['destDir'];
86 }
87 if (!is_dir($testFilePath)) {
88 echo " - skipped (directory not found: {$testFilePath})\n";
89 } else {
90 $testFilePath .= '/' . $testFile['destFile'];
91 if (is_file($testFilePath)) {
92 unlink($testFilePath);
93 }
94 $sourceData->fetchSupplementalFile($testFile['source'], $testFilePath);
95 echo "done.\n";
96 }
97 }
98 }
99 }
100 }
101 exit(0);
102} catch (Exception $x) {
103 echo "\n", $x->getMessage(), "\n";
104 if (!$x instanceof UserMessageException) {
105 echo 'FILE: ', $x->getFile(), '@', $x->getLine(), "\n";
106 if (method_exists($x, 'getTraceAsString')) {
107 echo "TRACE:\n", $x->getTraceAsString(), "\n";
108 }
109 }
110 exit(1);
111}
112
113class UserMessageException extends Exception
114{
115}
116
117/**
118 * Command options.
119 */
120class Options
121{
122 /**
123 * The data format version: to be increased when the data structure changes (not needed if data is only added).
124 *
125 * @var string
126 */
127 const FORMAT_VERSION = '1';
128
129 /**
130 * The default location where the Punic data is stored.
131 *
132 * @var string
133 */
134 const DEFAULT_SOURCE_LOCATION = 'https://punic.github.io/data';
135
136 /**
137 * Comma-separated list of the default locales.
138 *
139 * @var string
140 */
141 const DEFAULT_LOCALES = 'ar,ca,cs,da,de,el,en,en_AU,en_CA,en_GB,en_HK,en_IN,es,fi,fr,he,hi,hr,hu,it,ja,ko,nb,nl,nn,pl,pt,pt_PT,ro,root,ru,sk,sl,sr,sv,th,tr,uk,vi,zh,zh_Hant';
142
143 /**
144 * Placeholder for all locales.
145 *
146 * @var string
147 */
148 const ALL_LOCALES_PLACEHOLDER = '[ALL]';
149
150 /**
151 * The location where the Punic data is stored.
152 *
153 * @var string
154 */
155 protected $sourceLocation;
156
157 /**
158 * Get the location where the Punic data is stored.
159 *
160 * @return string
161 */
162 public function getSourceLocation()
163 {
164 return $this->sourceLocation;
165 }
166
167
168 /**
169 * The CLDR version (null: latest).
170 *
171 * @var string|null
172 */
173 protected $cldrVersion;
174
175 /**
176 * Get the CLDR version (null: latest).
177 *
178 * @return string|null
179 */
180 public function getCLDRVersion()
181 {
182 return $this->cldrVersion;
183 }
184
185 /**
186 * The Punic data should be reset?
187 *
188 * @var bool
189 */
190 protected $resetPunicData;
191
192 /**
193 * The Punic data should be reset?
194 *
195 * @return bool
196 */
197 public function getResetPunicData()
198 {
199 return $this->resetPunicData;
200 }
201
202 /**
203 * The list of the output locales (or true for all).
204 *
205 * @var string[]|true
206 */
207 protected $locales;
208
209 /**
210 * The list of the locales to exclude.
211 *
212 * @var string[]|true
213 */
214 protected $excludeLocales;
215
216 /**
217 * The output directory.
218 *
219 * @var string
220 */
221 protected $outputDirectory;
222
223 /**
224 * Get the output directory.
225 *
226 * @return string
227 */
228 public function getOutputDirectory()
229 {
230 return $this->outputDirectory;
231 }
232
233 /**
234 * @return string
235 */
236 protected static function getDefaultOutputDirectory()
237 {
238 return rtrim(str_replace(DIRECTORY_SEPARATOR, '/', dirname(dirname(__FILE__))), '/').'/src/data';
239 }
240
241
242 /**
243 * @var bool
244 */
245 protected $shouldUpdateTestFiles;
246
247 /**
248 * Should the test files be updated?
249 *
250 * @return bool
251 */
252 public function shouldUpdateTestFiles()
253 {
254 return $this->shouldUpdateTestFiles;
255 }
256
257 /**
258 * @return string
259 */
260 public static function getPunicTestsDirectory()
261 {
262 return dirname(dirname(self::getDefaultOutputDirectory())) . '/tests';
263 }
264
265 /**
266 * Initializes the instance.
267 */
268 protected function __construct()
269 {
270 $this->sourceLocation = static::DEFAULT_SOURCE_LOCATION;
271 $this->cldrVersion = null;
272 $this->resetPunicData = false;
273 $this->locales = explode(',', static::DEFAULT_LOCALES);
274 $this->excludeLocales = array();
275 $this->outputDirectory = static::getDefaultOutputDirectory();
276 $this->shouldUpdateTestFiles = false;
277 }
278
279 /**
280 * @param array $options
281 *
282 * @return static
283 */
284 public static function fromArray(array $options)
285 {
286 $result = new static();
287 $localeOptions = array();
288 $n = count($options);
289 for ($i = 0; $i < $n; ++$i) {
290 if (preg_match('/^(--[^=]+)=(.*)$/', $options[$i], $matches)) {
291 $currentOption = $matches[1];
292 $nextOption = $matches[2];
293 $advanceNext = false;
294 } else {
295 $currentOption = $options[$i];
296 $nextOption = $i + 1 < $n ? $options[$i + 1] : '';
297 $advanceNext = true;
298 }
299
300 $optionWithValue = false;
301 switch (strtolower($currentOption)) {
302 case '-h':
303 case '--help':
304 static::showHelp();
305 exit(0);
306 case '--source-location':
307 case '-s':
308 $optionWithValue = true;
309 if ($nextOption === '') {
310 throw new UserMessageException('Please specify the location of the source data');
311 }
312 $result->sourceLocation = $nextOption;
313 break;
314 case '--version':
315 case '-v':
316 $optionWithValue = true;
317 if ($nextOption === '') {
318 throw new UserMessageException('Please specify the CLDR version to be processed');
319 }
320 if (!preg_match('/^[1-9]\d*(\.\d+)*(\.[dM]\d+|\.beta\.\d+)?$/', $nextOption)) {
321 throw new UserMessageException("Invalid version specified ({$nextOption})");
322 }
323 $result->cldrVersion = $nextOption;
324 break;
325 case '--locale':
326 case '-l':
327 $optionWithValue = true;
328 if ($nextOption === '') {
329 throw new UserMessageException('Please specify one or more locale identifiers');
330 }
331 $localeOptions = array_merge($localeOptions, explode(',', $nextOption));
332 break;
333 case '--reset':
334 case '-r':
335 $result->resetPunicData = true;
336 break;
337 case '--output':
338 case '-o':
339 $optionWithValue = true;
340 if ($nextOption === '') {
341 throw new UserMessageException('Please specify the output directory');
342 }
343 $s = static::normalizeDirectoryPath($nextOption);
344 if ($s === null) {
345 throw new UserMessageException("{$currentOption} is not a valid output directory path");
346 }
347 $result->outputDirectory = $s;
348 break;
349 case '--update-test-files':
350 case '-t':
351 $result->shouldUpdateTestFiles = true;
352 break;
353 default:
354 throw new UserMessageException("Unknown option: {$currentOption}\nUse -h (or --help) to get the list of available options");
355 }
356 if ($optionWithValue && $advanceNext) {
357 ++$i;
358 }
359 }
360 if (!empty($localeOptions)) {
361 $result->parseLocaleOptions($localeOptions);
362 }
363
364 return $result;
365 }
366
367 /**
368 * @param string|mixed $path
369 *
370 * @return string|null
371 */
372 protected static function normalizeDirectoryPath($path)
373 {
374 $result = null;
375 if (is_string($path)) {
376 $path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
377 if (stripos(PHP_OS, 'WIN') === 0) {
378 $invalidChars = implode('', array_map('chr', range(0, 31))).'*?"<>|';
379 } else {
380 $invalidChars = '';
381 }
382 $path = rtrim($path, '/');
383 if ($path !== '' && $invalidChars === '' || strpbrk($path, $invalidChars) === false) {
384 $result = $path;
385 }
386 }
387
388 return $result;
389 }
390
391 /**
392 * @param array $localeOptions
393 *
394 * @throws Exception
395 */
396 protected function parseLocaleOptions(array $localeOptions)
397 {
398 $allLocales = false;
399 $locales = array();
400 foreach ($localeOptions as $localeOption) {
401 if ($localeOption === '') {
402 throw new Exception('Empty locale detected');
403 }
404 if ($localeOption === 'root') {
405 $localeOption = 'en_US';
406 } elseif ($localeOption === static::ALL_LOCALES_PLACEHOLDER) {
407 $allLocales = true;
408 } else {
409 $localeOperation = '=';
410 $localeCode = $localeOption;
411 if ($localeOption !== '') {
412 switch ($localeOption[0]) {
413 case '+':
414 case '-':
415 $localeOperation = $localeOption[0];
416 $localeCode = substr($localeOption, 1);
417 break;
418 }
419 }
420 $locale = LocaleIdentifier::fromString($localeCode);
421 if ($locale === null) {
422 throw new Exception("Invalid locale identifier specified: {$localeOption}");
423 }
424 $localeCode = (string) $locale;
425 if (isset($locales[$localeCode])) {
426 throw new Exception("Locale identifier specified more than once: {$localeCode}");
427 }
428 $locales[$localeCode] = $localeOperation;
429 }
430 }
431 if ($allLocales) {
432 $this->locales = true;
433 if (in_array('=', $locales, true)) {
434 throw new Exception("You specified to use all the locales, and to use specific locales.\nIf you want to specify 'all locales except some', please prepend them with a minus sign.");
435 }
436 $this->excludeLocales = array_keys(array_filter(
437 $locales,
438 function ($operation) {
439 return $operation === '-';
440 }
441 ));
442 } else {
443 if (in_array('=', $locales, true)) {
444 $this->locales = array_keys(array_filter(
445 $locales,
446 function ($operation) {
447 return $operation !== '-';
448 }
449 ));
450 } else {
451 $this->locales = array_values(array_unique(array_merge(
452 $this->locales,
453 array_keys(array_filter(
454 $locales,
455 function ($operation) {
456 return $operation === '+';
457 }
458 ))
459 )));
460 }
461 $this->excludeLocales = array_keys(array_filter(
462 $locales,
463 function ($operation) {
464 return $operation === '-';
465 }
466 ));
467 }
468 if ($this->locales !== true && !empty($this->excludeLocales)) {
469 $common = array_intersect($this->locales, $this->excludeLocales);
470 if (!empty($common)) {
471 $this->locales = array_values(array_diff($this->locales, $common));
472 }
473 $this->excludeLocales = array();
474 }
475 }
476
477 /**
478 * @return string
479 */
480 public function describeLocales()
481 {
482 if ($this->locales === true) {
483 if (empty($this->excludeLocales)) {
484 $result = 'all locales';
485 } else {
486 $result = 'all locales except '.implode(', ', $this->excludeLocales);
487 }
488 } else {
489 $result = implode(', ', $this->locales);
490 }
491
492 return $result;
493 }
494
495 protected static function showHelp()
496 {
497 $defaultSourceLocation = static::DEFAULT_SOURCE_LOCATION;
498 $allLocalesPlaceholders = static::ALL_LOCALES_PLACEHOLDER;
499 $defaultLocales = static::DEFAULT_LOCALES;
500 $defaultOutputDirectory = str_replace('/', DIRECTORY_SEPARATOR, static::getDefaultOutputDirectory());
501 echo <<<EOT
502Available options:
503
504 --help|-h
505 Show this help message
506
507 --version=<version>|-v <version>
508 Set the CLDR version to work on (default: latest available)
509 Examples: 31.d02 30.0.3 30 29.beta.1 25.M1 23.1.d01
510
511 --source-location=<location>|-s <location>
512 The location of the Punic data (default: {$defaultSourceLocation})
513
514 --reset|-r
515 Reset the destination Punic data before the execution
516
517 --output|-o
518 Set the output directory (default: {$defaultOutputDirectory})
519
520 --update-test-files|-t
521 Update the Punic test files (useful only for Punic developers)
522
523 --locale=<locales>|-l <locales>
524 Set the locales to work on.
525 It's a comman-separated list of locale codes (you can also specify this option multiple times).
526 You can use {$allLocalesPlaceholders} (case-sensitive) to include all available locales.
527 You can prepend a minus sign to substract specific locales: so for instance
528 --locale=-it,-de
529 means 'the default locales except Italian and German'.
530 Likewise:
531 --locale={$allLocalesPlaceholders},-it,-de
532 means 'all locales except Italian and German'.
533 You can prepend a plus to add specific locales: so for instance
534 --locale=+it,+de
535 means 'default locales plus Italian and German'.
536 The locales included by default are:
537 {$defaultLocales}
538
539EOT;
540 }
541
542 /**
543 * @param string[] $availableLocales
544 *
545 * @throws Exception
546 *
547 * @return string[]
548 */
549 public function finalizeLocalesList(array $availableLocales)
550 {
551 if ($this->locales === true) {
552 $locales = $availableLocales;
553 } else {
554 foreach ($this->locales as $locale) {
555 if (in_array($locale, $availableLocales, true) !== true) {
556 throw new UserMessageException("The locale {$locale} is not supported.\nHere's the list of available locales:\n- " . implode("\n- ", $availableLocales));
557 }
558 }
559 $locales = $this->locales;
560 }
561 if (!empty($this->excludeLocales)) {
562 $locales = array_diff($locales, $this->excludeLocales);
563 }
564 natcasesort($locales);
565
566 return array_values($locales);
567 }
568}
569
570class FileUtils
571{
572 /**
573 * @param string $path
574 * @param bool $emptyOnlyDir
575 *
576 * @throws Exception
577 */
578 public function deleteFromFilesystem($path, $emptyOnlyDir = false)
579 {
580 $maxRetries = 5;
581 if (is_file($path) || is_link($path)) {
582 for ($i = 1; ; $i++) {
583 if (@unlink($path) === false) {
584 if ($i === $maxRetries) {
585 throw new Exception("Failed to delete the file {$path}");
586 }
587 } else {
588 break;
589 }
590 }
591 } elseif (is_dir($path)) {
592 $contents = @scandir($path);
593 if ($contents === false) {
594 throw new Exception("Failed to retrieve the contents of the directory {$path}");
595 }
596 foreach (array_diff($contents, array('.', '..')) as $item) {
597 $this->deleteFromFilesystem($path.'/'.$item);
598 }
599 if (!$emptyOnlyDir) {
600 for ($i = 1; ; $i++) {
601 if (@rmdir($path) === false) {
602 if ($i === $maxRetries) {
603 throw new Exception("Failed to delete the directory {$path}");
604 }
605 } else {
606 break;
607 }
608 }
609 }
610 }
611 }
612
613 /**
614 * @param string $path
615 *
616 * @throws Exception
617 */
618 public function createDirectory($path)
619 {
620 if (!is_dir($path)) {
621 if (@mkdir($path, 0777, true) !== true) {
622 throw new Exception("Failed to create the directory {$path}");
623 }
624 }
625 }
626}
627
628class SourceData
629{
630 /**
631 * @var FileUtils
632 */
633 protected $fileUtils;
634
635 /**
636 * @var Courier
637 */
638 protected $courier;
639
640 /**
641 * @var array|null
642 */
643 protected $state;
644
645 /**
646 * @var null|string
647 */
648 protected $cldrVersion;
649
650 /**
651 * @var string[]|null
652 */
653 protected $availableLocales;
654
655 /**
656 * @var string[]|null
657 */
658 protected $localeFiles;
659
660 /**
661 * @var string[]|null
662 */
663 protected $supplementalFiles;
664
665 /**
666 * @var string[]|null
667 */
668 protected $testFiles;
669
670 /**
671 * @param Options $options
672 */
673 public function __construct(FileUtils $fileUtils, Courier $courier)
674 {
675 $this->fileUtils = $fileUtils;
676 $this->courier = $courier;
677 $this->state = null;
678 $this->cldrVersion = null;
679 $this->availableLocales = null;
680 $this->localeFiles = null;
681 $this->supplementalFiles = null;
682 $this->testFiles = null;
683 }
684
685 /**
686 * @throws Exception
687 */
688 public function readState()
689 {
690 if ($this->state === null) {
691 $state = $this->courier->getJson('state.json');
692 if (!isset($state['formats'])) {
693 throw new Exception('Invalid state.json file (missing formats).');
694 }
695 if (!isset($state['formats'][Options::FORMAT_VERSION])) {
696 throw new Exception('Invalid state.json file (missing current format)');
697 }
698 $this->state = $state['formats'][Options::FORMAT_VERSION];
699 }
700 }
701
702 /**
703 * @throws Exception
704 *
705 * @return string
706 */
707 public function getLatestCLDRVersion()
708 {
709 $this->readState();
710 $latest = null;
711 $all = isset($this->state['cldr']) ? array_keys($this->state['cldr']) : array();
712 foreach ($all as $v) {
713 $v = (string) $v;
714 if ($latest === null || version_compare($latest, $v) < 0) {
715 $latest = $v;
716 }
717 }
718 if ($latest === null) {
719 throw new Exception('No CLDR version found!');
720 }
721
722 return $latest;
723 }
724
725 /**
726 * @param string $version
727 *
728 * @throws UserMessageException
729 */
730 public function setCLDRVersion($version)
731 {
732 $this->readState();
733 if (is_string($version) && isset($this->state['cldr']) && is_array($this->state['cldr']) && isset($this->state['cldr'][$version])) {
734 $this->cldrVersion = $version;
735 $this->availableLocales = null;
736 $this->localeFiles = null;
737 $this->supplementalFiles = null;
738 $this->testFiles = null;
739 } else {
740 throw new UserMessageException("Invalid CLDR version: {$version}");
741 }
742 }
743
744 /**
745 * Get the list of available locale IDs.
746 *
747 * @throws Exception
748 *
749 * @return string[]
750 */
751 public function getAvailableLocales()
752 {
753 if ($this->availableLocales === null) {
754 $this->readState();
755 if ($this->cldrVersion === null) {
756 throw new Exception('CLDR version not set');
757 }
758 $data = $this->state['cldr'][$this->cldrVersion];
759 if (!isset($data['locales']) || !is_array($data['locales']) || count($data['locales']) === 0) {
760 throw new Exception('Missing locales in state file');
761 }
762 $availableLocales = $data['locales'];
763 natcasesort($availableLocales);
764 $this->availableLocales = $availableLocales;
765 }
766 return $this->availableLocales;
767 }
768
769 /**
770 * Get the list of locale-specific file names.
771 *
772 * @throws Exception
773 *
774 * @return string[]
775 */
776 public function getLocaleFiles()
777 {
778 if ($this->localeFiles === null) {
779 $this->readState();
780 if ($this->cldrVersion === null) {
781 throw new Exception('CLDR version not set');
782 }
783 $data = $this->state['cldr'][$this->cldrVersion];
784 if (!isset($data['localeFiles']) || !is_array($data['localeFiles']) || count($data['localeFiles']) === 0) {
785 throw new Exception('Missing localeFiles in state file');
786 }
787 $localeFiles = $data['localeFiles'];
788 natcasesort($localeFiles);
789 $this->localeFiles = $localeFiles;
790 }
791 return $this->localeFiles;
792 }
793
794
795 /**
796 * Get the list of supplemental file names.
797 *
798 * @throws Exception
799 *
800 * @return string[]
801 */
802 public function getSupplementalFiles()
803 {
804 if ($this->supplementalFiles === null) {
805 $this->readState();
806 if ($this->cldrVersion === null) {
807 throw new Exception('CLDR version not set');
808 }
809 $data = $this->state['cldr'][$this->cldrVersion];
810 if (!isset($data['supplementalFiles']) || !is_array($data['supplementalFiles']) || count($data['supplementalFiles']) === 0) {
811 throw new Exception('Missing supplementalFiles in state file');
812 }
813 $supplementalFiles = $data['supplementalFiles'];
814 natcasesort($supplementalFiles);
815 $this->supplementalFiles = $supplementalFiles;
816 }
817 return $this->supplementalFiles;
818 }
819
820 /**
821 * Get the list of test file names and their path relative to the test direcory.
822 *
823 * @throws Exception
824 *
825 * @return array
826 */
827 public function getTestFiles()
828 {
829 if ($this->testFiles === null) {
830 $this->readState();
831 if ($this->cldrVersion === null) {
832 throw new Exception('CLDR version not set');
833 }
834 $data = $this->state['cldr'][$this->cldrVersion];
835 $testFiles = array();
836 if (isset($data['testFiles']) && is_array($data['testFiles'])) {
837 foreach ($data['testFiles'] as $testFile) {
838 switch ($testFile) {
839 case '__test.plurals.php':
840 $testFiles[] = array('source' => $testFile, 'destDir' => 'dataFiles', 'destFile' => 'plurals.php');
841 break;
842 }
843 }
844 }
845 $this->testFiles = $testFiles;
846 }
847 return $this->testFiles;
848 }
849
850 /**
851 * @param string $localeID
852 * @param string $destDir
853 *
854 * @throws Exception
855 */
856 public function fetchLocale($localeID, $destDir)
857 {
858 $this->fileUtils->deleteFromFilesystem($destDir);
859 $this->fileUtils->createDirectory($destDir);
860 try {
861 foreach ($this->getLocaleFiles() as $localeFile) {
862 $data = $this->courier->getPhp(Options::FORMAT_VERSION . '/' . $this->cldrVersion . '/' . str_replace('_', '-', $localeID) . '/' . $localeFile);
863 if (@file_put_contents($destDir . '/' . $localeFile, $data) !== strlen($data)) {
864 throw new Exception("Failed to save file {$localeFile}");
865 }
866 }
867 } catch (Exception $x) {
868 try {
869 $this->fileUtils->deleteFromFilesystem($destDir);
870 } catch (Exception $foo) {
871 }
872 throw $x;
873 }
874 }
875
876 /**
877 * @param string $localeID
878 * @param string $destDir
879 *
880 * @throws Exception
881 */
882 public function fetchSupplementalFile($supplementalFile, $destFile)
883 {
884 $this->fileUtils->deleteFromFilesystem($destFile);
885 try {
886 $data = $this->courier->getPhp(Options::FORMAT_VERSION . '/' . $this->cldrVersion . '/' . $supplementalFile);
887 if (@file_put_contents($destFile, $data) !== strlen($data)) {
888 throw new Exception("Failed to save file {$supplementalFile}");
889 }
890 } catch (Exception $x) {
891 try {
892 $this->fileUtils->deleteFromFilesystem($destFile);
893 } catch (Exception $foo) {
894 }
895 throw $x;
896 }
897 }
898}
899
900class LocaleIdentifier
901{
902 /**
903 * @var string
904 */
905 protected $language = '';
906
907 /**
908 * @var string
909 */
910 protected $script = '';
911
912 /**
913 * @var string
914 */
915 protected $region = '';
916
917 /**
918 * @var string
919 */
920 protected $variants = array();
921
922 protected function __construct()
923 {
924 }
925
926 /**
927 * @param string|mixed $localeIdentifier
928 *
929 * @return static|null
930 */
931 public static function fromString($localeIdentifier)
932 {
933 // http://unicode.org/reports/tr35/#Unicode_language_identifier
934 if (strcasecmp($localeIdentifier, 'root') === 0) {
935 $result = new static();
936 $result->language = 'root';
937 } else {
938 $rxLanguage = '(?:[a-z]{2,3})|(?:[a-z]{5,8}:)';
939 $rxScript = '[a-z]{4}';
940 $rxRegion = '(?:[a-z]{2})|(?:[0-9]{3})';
941 $rxVariant = '(?:[a-z0-9]{5,8})|(?:[0-9][a-z0-9]{3})';
942 $rxSep = '[-_]';
943 if (is_string($localeIdentifier) && preg_match("/^($rxLanguage)(?:$rxSep($rxScript))?(?:$rxSep($rxRegion))?((?:$rxSep(?:$rxVariant))*)$/i", $localeIdentifier, $matches)) {
944 $result = new static();
945 $result->language = strtolower($matches[1]);
946 if (isset($matches[2])) {
947 $result->script = ucfirst(strtolower($matches[2]));
948 }
949 if (isset($matches[3])) {
950 $result->region = strtoupper($matches[3]);
951 }
952 if ($matches[4] !== '') {
953 $result->variants = explode('_', strtoupper(str_replace('-', '_', substr($matches[4], 1))));
954 }
955 } else {
956 $result = null;
957 }
958 }
959
960 return $result;
961 }
962
963 protected static function merge($language, $script = '', $region = '', array $variants = array())
964 {
965 $parts = array();
966
967 $parts[] = $language;
968 if ($script !== '') {
969 $parts[] = $script;
970 }
971 if ($region !== '') {
972 $parts[] = $region;
973 }
974 $parts = array_merge($parts, $variants);
975
976 return implode('_', $parts);
977 }
978
979 /**
980 * @return string
981 */
982 public function __toString()
983 {
984 return static::merge($this->language, $this->script, $this->region, $this->variants);
985 }
986}
987
988class Courier
989{
990 /**
991 * @var Options
992 */
993 protected $options;
994
995 /**
996 * @param Options $options
997 */
998 public function __construct(Options $options)
999 {
1000 $this->options = $options;
1001 }
1002
1003 /**
1004 * @param string $relativePath
1005 *
1006 * @throws Exception
1007 *
1008 * @return string
1009 */
1010 public function getFile($relativePath)
1011 {
1012 $fullPath = rtrim($this->options->getSourceLocation(), '/') . '/' . ltrim($relativePath, '/');
1013 $contents = file_get_contents($fullPath);
1014 if (isset($http_response_header)) {
1015 foreach($http_response_header as $header) {
1016 if (preg_match("/^HTTP\/.+?\s+([0-9]+)(\s+(\S.+)?)$/", $header, $matches)) {
1017 $code = (int) $matches[1];
1018 if ($code !== 200) {
1019 $message = isset($matches[2]) ? trim($matches[2]) : '';
1020 throw new Exception("Failed to fetch {$fullPath}. HTTP error code: {$code}" . ($message === '' ? '' : " ($message)"));
1021 }
1022 }
1023 break;
1024 }
1025 }
1026 if ($contents === false) {
1027 throw new Exception("Failed to read {$fullPath}");
1028 }
1029
1030 return $contents;
1031 }
1032
1033 /**
1034 * @param string $relativePath
1035 *
1036 * @throws Exception
1037 *
1038 * @return array
1039 */
1040 public function getJson($relativePath)
1041 {
1042 $json = $this->getFile($relativePath);
1043 $result = @json_decode($json, true);
1044 if (!is_array($result)) {
1045 throw new Exception("Failed to decode JSON from {$relativePath}");
1046 }
1047 return $result;
1048 }
1049
1050
1051 /**
1052 * @param string $relativePath
1053 *
1054 * @throws Exception
1055 *
1056 * @return array
1057 */
1058 public function getPhp($relativePath)
1059 {
1060 $php = $this->getFile($relativePath);
1061 if (strpos($php, '<?php') !== 0) {
1062 throw new Exception("Failed fetch PHP file from {$relativePath}");
1063 }
1064 return $php;
1065 }
1066}
1067