Development

/branches/1.4/lib/i18n/sfCultureInfo.class.php

You must first sign up to be able to contribute.

root/branches/1.4/lib/i18n/sfCultureInfo.class.php

Revision 33361, 22.1 kB (checked in by fabien, 2 years ago)

[1.4] fixed previous commit

  • Property svn:mime-type set to text/x-php
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1 <?php
2
3 /**
4  * sfCultureInfo class file.
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the BSD License.
8  *
9  * Copyright(c) 2004 by Qiang Xue. All rights reserved.
10  *
11  * To contact the author write to {@link mailto:qiang.xue@gmail.com Qiang Xue}
12  * The latest version of PRADO can be obtained from:
13  * {@link http://prado.sourceforge.net/}
14  *
15  * @author     Wei Zhuo <weizhuo[at]gmail[dot]com>
16  * @version    $Id$
17  * @package    symfony
18  * @subpackage i18n
19  */
20
21 /**
22  * sfCultureInfo class.
23  *
24  * Represents information about a specific culture including the
25  * names of the culture, the calendar used, as well as access to
26  * culture-specific objects that provide methods for common operations,
27  * such as formatting dates, numbers, and currency.
28  *
29  * The sfCultureInfo class holds culture-specific information, such as the
30  * associated language, sublanguage, country/region, calendar, and cultural
31  * conventions. This class also provides access to culture-specific
32  * instances of sfDateTimeFormatInfo and sfNumberFormatInfo. These objects
33  * contain the information required for culture-specific operations,
34  * such as formatting dates, numbers and currency.
35  *
36  * The culture names follow the format "<languagecode>_<country/regioncode>",
37  * where <languagecode> is a lowercase two-letter code derived from ISO 639
38  * codes. You can find a full list of the ISO-639 codes at
39  * http://www.ics.uci.edu/pub/ietf/http/related/iso639.txt
40  *
41  * The <country/regioncode2> is an uppercase two-letter code derived from
42  * ISO 3166. A copy of ISO-3166 can be found at
43  * http://www.chemie.fu-berlin.de/diverse/doc/ISO_3166.html
44  *
45  * For example, Australian English is "en_AU".
46  *
47  * @author Xiang Wei Zhuo <weizhuo[at]gmail[dot]com>
48  * @version v1.0, last update on Sat Dec 04 13:41:46 EST 2004
49  * @package    symfony
50  * @subpackage i18n
51  */
52 class sfCultureInfo
53 {
54   /**
55    * ICU data filename extension.
56    * @var string
57    */
58   protected $dataFileExt = '.dat';
59
60   /**
61    * The ICU data array.
62    * @var array
63    */
64   protected $data = array();
65
66   /**
67    * The current culture.
68    * @var string
69    */
70   protected $culture;
71
72   /**
73    * Directory where the ICU data is stored.
74    * @var string
75    */
76   protected $dataDir;
77
78   /**
79    * A list of ICU date files loaded.
80    * @var array
81    */
82   protected $dataFiles = array();
83
84   /**
85    * The current date time format info.
86    * @var sfDateTimeFormatInfo
87    */
88   protected $dateTimeFormat;
89
90   /**
91    * The current number format info.
92    * @var sfNumberFormatInfo
93    */
94   protected $numberFormat;
95  
96   /**
97    * A list of properties that are accessable/writable.
98    * @var array
99    */
100   protected $properties = array();
101
102   /**
103    * Culture type, all.
104    * @see getCultures()
105    * @var int
106    */
107   const ALL = 0;
108
109   /**
110    * Culture type, neutral.
111    * @see getCultures()
112    * @var int
113    */
114   const NEUTRAL = 1;
115
116   /**
117    * Culture type, specific.
118    *
119    * @see getCultures()
120    * @var int
121    */
122   const SPECIFIC = 2;
123
124   /**
125    * Gets the sfCultureInfo that for this culture string.
126    *
127    * @param string  $culture The culture for this instance
128    * @return sfCultureInfo Invariant culture info is "en"
129    */
130   public static function getInstance($culture = 'en')
131   {
132     static $instances = array();
133
134     if (!isset($instances[$culture]))
135     {
136       $instances[$culture] = new sfCultureInfo($culture);
137     }
138
139     return $instances[$culture];
140   }
141
142   /**
143    * Displays the culture name.
144    *
145    * @return string the culture name.
146    * @see getName()
147    */
148   public function __toString()
149   {
150     return $this->getName();
151   }
152
153   /**
154    * Allows functions that begins with 'set' to be called directly
155    * as an attribute/property to retrieve the value.
156    *
157    * @param string $name The property to get
158    * @return mixed
159    */
160   public function __get($name)
161   {
162     $getProperty = 'get'.$name;
163     if (in_array($getProperty, $this->properties))
164     {
165       return $this->$getProperty();
166     }
167     else
168     {
169       throw new sfException(sprintf('Property %s does not exists.', $name));
170     }
171   }
172
173   /**
174    * Allows functions that begins with 'set' to be called directly
175    * as an attribute/property to set the value.
176    *
177    * @param string $name  The property to set
178    * @param string $value The property value
179    */
180   public function __set($name, $value)
181   {
182     $setProperty = 'set'.$name;
183     if (in_array($setProperty, $this->properties))
184     {
185       $this->$setProperty($value);
186     }
187     else
188     {
189       throw new sfException(sprintf('Property %s can not be set.', $name));
190     }
191   }
192
193   /**
194    * Initializes a new instance of the sfCultureInfo class based on the
195    * culture specified by name. E.g. <code>new sfCultureInfo('en_AU');</code>
196    * The culture indentifier must be of the form
197    * "<language>_(country/region/variant)".
198    *
199    * @param string $culture a culture name, e.g. "en_AU".
200    * @return return new sfCultureInfo.
201    */
202   public function __construct($culture = 'en')
203   {
204     $this->properties = get_class_methods($this);
205
206     if (empty($culture))
207     {
208       $culture = 'en';
209     }
210
211     $this->dataDir = self::dataDir();
212     $this->dataFileExt = self::fileExt();
213
214     $this->setCulture($culture);
215
216     $this->loadCultureData('root');
217     $this->loadCultureData($culture);
218   }
219
220   /**
221    * Gets the default directory for the ICU data.
222    * The default is the "data" directory for this class.
223    *
224    * @return string directory containing the ICU data.
225    */
226   protected static function dataDir()
227   {
228     return dirname(__FILE__).'/data/';
229   }
230
231   /**
232    * Gets the filename extension for ICU data. Default is ".dat".
233    *
234    * @return string filename extension for ICU data.
235    */
236   protected static function fileExt()
237   {
238     return '.dat';
239   }
240
241   /**
242    * Determines if a given culture is valid. Simply checks that the
243    * culture data exists.
244    *
245    * @param string $culture a culture
246    * @return boolean true if valid, false otherwise.
247    */
248   static public function validCulture($culture)
249   {
250     if (preg_match('/^[a-z]{2}(_[A-Z]{2,5}){0,2}$/', $culture))
251     {
252       return is_file(self::dataDir().$culture.self::fileExt());
253     }
254
255     return false;
256   }
257
258   /**
259    * Sets the culture for the current instance. The culture indentifier
260    * must be of the form "<language>_(country/region)".
261    *
262    * @param string $culture culture identifier, e.g. "fr_FR_EURO".
263    */
264   protected function setCulture($culture)
265   {
266     if (!empty($culture))
267     {
268       if (!preg_match('/^[a-z]{2}(_[A-Z]{2,5}){0,2}$/', $culture))
269       {
270         throw new sfException(sprintf('Invalid culture supplied: %s', $culture));
271       }
272     }
273
274     $this->culture = $culture;
275   }
276
277   /**
278    * Loads the ICU culture data for the specific culture identifier.
279    *
280    * @param string $culture the culture identifier.
281    */
282   protected function loadCultureData($culture)
283   {
284     $file_parts = explode('_', $culture);
285     $current_part = $file_parts[0];
286
287     $files = array($current_part);
288
289     for ($i = 1, $max = count($file_parts); $i < $max; $i++)
290     {
291       $current_part .= '_'.$file_parts[$i];
292       $files[] = $current_part;
293     }
294
295     foreach ($files as $file)
296     {
297       $filename = $this->dataDir.$file.$this->dataFileExt;
298
299       if (is_file($filename) == false)
300       {
301         throw new sfException(sprintf('Data file for "%s" was not found.', $file));
302       }
303
304       if (in_array($filename, $this->dataFiles) == false)
305       {
306         array_unshift($this->dataFiles, $file);
307
308         $data = &$this->getData($filename);
309         $this->data[$file] = &$data;
310
311         if (isset($data['__ALIAS']))
312         {
313           $this->loadCultureData($data['__ALIAS']);
314         }
315         unset($data);
316       }
317     }
318   }
319
320   /**
321    * Gets the data by unserializing the ICU data from disk.
322    * The data files are cached in a static variable inside
323    * this function.
324    *
325    * @param string $filename the ICU data filename
326    * @return array ICU data
327    */
328   protected function &getData($filename)
329   {
330     static $data  = array();
331     static $files = array();
332
333     if (!in_array($filename, $files))
334     {
335       $data[$filename] = unserialize(file_get_contents($filename));
336       $files[] = $filename;
337     }
338
339     return $data[$filename];
340   }
341
342   /**
343    * Finds the specific ICU data information from the data.
344    * The path to the specific ICU data is separated with a slash "/".
345    * E.g. To find the default calendar used by the culture, the path
346    * "calendar/default" will return the corresponding default calendar.
347    * Use merge=true to return the ICU including the parent culture.
348    * E.g. The currency data for a variant, say "en_AU" contains one
349    * entry, the currency for AUD, the other currency data are stored
350    * in the "en" data file. Thus to retrieve all the data regarding
351    * currency for "en_AU", you need to use findInfo("Currencies,true);.
352    *
353    * @param string  $path   the data you want to find.
354    * @param boolean $merge  merge the data from its parents.
355    * @return mixed the specific ICU data.
356    */
357   protected function findInfo($path = '/', $merge = false)
358   {
359     $result = array();
360     foreach ($this->dataFiles as $section)
361     {
362       $info = $this->searchArray($this->data[$section], $path);
363
364       if ($info)
365       {
366         if ($merge)
367         {
368           $result = $this->array_add($result, $info);
369         }
370         else
371         {
372           return $info;
373         }
374       }
375     }
376
377     return $result;
378   }
379
380   /**
381    * Adds an array to an already existing array.
382    * If an element is already existing in array1 it is not overwritten.
383    * If this element is an array this logic will be applied recursively.
384    */
385   private function array_add($array1, $array2)
386   {
387     foreach ($array2 as $key => $value)
388     {
389       if (isset($array1[$key]))
390       {
391         if(is_array($array1[$key]) && is_array($value))
392         {
393           $array1[$key] = $this->array_add($array1[$key], $value);
394         }
395       }
396       else
397       {
398         $array1[$key] = $value;
399       }
400     }
401     return $array1;
402   }
403
404   /**
405    * Searches the array for a specific value using a path separated using
406    * slash "/" separated path. e.g to find $info['hello']['world'],
407    * the path "hello/world" will return the corresponding value.
408    *
409    * @param array   $info  the array for search
410    * @param string  $path  slash "/" separated array path.
411    * @return mixed the value array using the path
412    */
413   protected function searchArray($info, $path = '/')
414   {
415     $index = explode('/', $path);
416
417     $array = $info;
418
419     for ($i = 0, $max = count($index); $i < $max; $i++)
420     {
421       $k = $index[$i];
422       if ($i < $max - 1 && isset($array[$k]))
423       {
424         $array = $array[$k];
425       }
426       else if ($i == $max - 1 && isset($array[$k]))
427       {
428         return $array[$k];
429       }
430     }
431   }
432  
433   /**
434    * Gets the culture name in the format
435    * "<languagecode2>_(country/regioncode2)".
436    *
437    * @return string culture name.
438    */
439   public function getName()
440   {
441     return $this->culture;
442   }
443
444   /**
445    * Gets the sfDateTimeFormatInfo that defines the culturally appropriate
446    * format of displaying dates and times.
447    *
448    * @return sfDateTimeFormatInfo date time format information for the culture.
449    */
450   public function getDateTimeFormat()
451   {
452     if (null === $this->dateTimeFormat)
453     {
454       $calendar = $this->getCalendar();
455       $info = $this->findInfo("calendar/{$calendar}", true);
456       $this->setDateTimeFormat(new sfDateTimeFormatInfo($info));
457     }
458
459     return $this->dateTimeFormat;
460   }
461
462   /**
463    * Sets the date time format information.
464    *
465    * @param sfDateTimeFormatInfo $dateTimeFormat the new date time format info.
466    */
467   public function setDateTimeFormat($dateTimeFormat)
468   {
469     $this->dateTimeFormat = $dateTimeFormat;
470   }
471
472   /**
473    * Gets the default calendar used by the culture, e.g. "gregorian".
474    *
475    * @return string the default calendar.
476    */
477   public function getCalendar()
478   {
479     return $this->findInfo('calendar/default');
480   }
481
482   /**
483    * Gets the culture name in the language that the culture is set
484    * to display. Returns <code>array('Language','Country');</code>
485    * 'Country' is omitted if the culture is neutral.
486    *
487    * @return array array with language and country as elements, localized.
488    */
489   public function getNativeName()
490   {
491     $lang = substr($this->culture, 0, 2);
492     $reg = substr($this->culture, 3, 2);
493     $language = $this->findInfo("Languages/{$lang}");
494     $region = $this->findInfo("Countries/{$reg}");
495     if ($region)
496     {
497       return $language.' ('.$region.')';
498     }
499     else
500     {
501       return $language;
502     }
503   }
504
505   /**
506    * Gets the culture name in English.
507    * Returns <code>array('Language','Country');</code>
508    * 'Country' is omitted if the culture is neutral.
509    *
510    * @return array array with language and country as elements.
511    */
512   public function getEnglishName()
513   {
514     $lang = substr($this->culture, 0, 2);
515     $reg = substr($this->culture, 3, 2);
516     $culture = $this->getInvariantCulture();
517
518     $language = $culture->findInfo("Languages/{$lang}");
519     if (count($language) == 0)
520     {
521       return $this->culture;
522     }
523
524     $region = $culture->findInfo("Countries/{$reg}");
525
526     return $region ? $language.' ('.$region.')' : $language;
527   }
528
529   /**
530    * Gets the sfCultureInfo that is culture-independent (invariant).
531    * Any changes to the invariant culture affects all other
532    * instances of the invariant culture.
533    * The invariant culture is assumed to be "en";
534    *
535    * @return sfCultureInfo invariant culture info is "en".
536    */
537   static function getInvariantCulture()
538   {
539     static $invariant;
540
541     if (null === $invariant)
542     {
543       $invariant = new sfCultureInfo();
544     }
545
546     return $invariant;
547   }
548
549   /**
550    * Gets a value indicating whether the current sfCultureInfo
551    * represents a neutral culture. Returns true if the culture
552    * only contains two characters.
553    *
554    * @return boolean true if culture is neutral, false otherwise.
555    */
556   public function getIsNeutralCulture()
557   {
558     return strlen($this->culture) == 2;
559   }
560
561   /**
562    * Gets the sfNumberFormatInfo that defines the culturally appropriate
563    * format of displaying numbers, currency, and percentage.
564    *
565    * @return sfNumberFormatInfo the number format info for current culture.
566    */
567   public function getNumberFormat()
568   {
569     if (null === $this->numberFormat)
570     {
571       $elements = $this->findInfo('NumberElements');
572       $patterns = $this->findInfo('NumberPatterns');
573       $currencies = $this->getCurrencies(null, true);
574       $data = array('NumberElements' => $elements, 'NumberPatterns' => $patterns, 'Currencies' => $currencies);
575
576       $this->setNumberFormat(new sfNumberFormatInfo($data));
577     }
578
579     return $this->numberFormat;
580   }
581
582   /**
583    * Sets the number format information.
584    *
585    * @param sfNumberFormatInfo $numberFormat the new number format info.
586    */
587   public function setNumberFormat($numberFormat)
588   {
589     $this->numberFormat = $numberFormat;
590   }
591
592   /**
593    * Gets the sfCultureInfo that represents the parent culture of the
594    * current sfCultureInfo
595    *
596    * @return sfCultureInfo parent culture information.
597    */
598   public function getParent()
599   {
600     if (strlen($this->culture) == 2)
601     {
602       return $this->getInvariantCulture();
603     }
604
605     return new sfCultureInfo(substr($this->culture, 0, 2));
606   }
607
608   /**
609    * Gets the list of supported cultures filtered by the specified
610    * culture type. This is an EXPENSIVE function, it needs to traverse
611    * a list of ICU files in the data directory.
612    * This function can be called statically.
613    *
614    * @param int $type culture type, sfCultureInfo::ALL, sfCultureInfo::NEUTRAL
615    * or sfCultureInfo::SPECIFIC.
616    * @return array list of culture information available.
617    */
618   static function getCultures($type = sfCultureInfo::ALL)
619   {
620     $dataDir = sfCultureInfo::dataDir();
621     $dataExt = sfCultureInfo::fileExt();
622     $dir = dir($dataDir);
623
624     $neutral = array();
625     $specific = array();
626
627     while (false !== ($entry = $dir->read()))
628     {
629       if (is_file($dataDir.$entry) && substr($entry, -4) == $dataExt && $entry != 'root'.$dataExt)
630       {
631         $culture = substr($entry, 0, -4);
632         if (strlen($culture) == 2)
633         {
634           $neutral[] = $culture;
635         }
636         else
637         {
638           $specific[] = $culture;
639         }
640       }
641     }
642     $dir->close();
643
644     switch ($type)
645     {
646       case sfCultureInfo::ALL:
647         $all array_merge($neutral, $specific);
648         sort($all);
649         return $all;
650         break;
651       case sfCultureInfo::NEUTRAL:
652         return $neutral;
653         break;
654       case sfCultureInfo::SPECIFIC:
655         return $specific;
656         break;
657     }
658   }
659
660   /**
661    * Get the country name in the current culture for the given code.
662    *
663    * @param  string $code A valid country code
664    *
665    * @return string The country name in the current culture
666    */
667   public function getCountry($code)
668   {
669     $countries = $this->findInfo('Countries', true);
670
671     if (!isset($countries[$code]))
672     {
673       throw new InvalidArgumentException(sprintf('The country %s does not exist.', $code));
674     }
675
676     return $countries[$code];
677   }
678
679   /**
680    * Get the currency name in the current culture for the given code.
681    *
682    * @param  string $code A valid currency code
683    *
684    * @return string The currency name in the current culture
685    */
686   public function getCurrency($code)
687   {
688     $currencies = $this->findInfo('Currencies', true);
689
690     if (!isset($currencies[$code]))
691     {
692       throw new InvalidArgumentException(sprintf('The currency %s does not exist.', $code));
693     }
694
695     return $currencies[$code][1];
696   }
697
698   /**
699    * Get the language name in the current culture for the given code.
700    *
701    * @param  string $code A valid language code
702    *
703    * @return string The language name in the current culture
704    */
705   public function getLanguage($code)
706   {
707     $languages = $this->findInfo('Languages', true);
708
709     if (!isset($languages[$code]))
710     {
711       throw new InvalidArgumentException(sprintf('The language %s does not exist.', $code));
712     }
713
714     return $languages[$code];
715   }
716
717   /**
718    * Gets a list of countries in the language of the localized version.
719    *
720    * @param  array $countries An array of countries used to restrict the returned array (null by default, which means all countries)
721    *
722    * @return array a list of localized country names.
723    */
724   public function getCountries($countries = null)
725   {
726     // remove integer keys as they do not represent countries
727     $allCountries = array();
728     foreach ($this->findInfo('Countries', true) as $key => $value)
729     {
730       if (!is_int($key))
731       {
732         $allCountries[$key] = $value;
733       }
734     }
735
736     // restrict countries to a sub-set
737     if (null !== $countries)
738     {
739       if ($problems = array_diff($countries, array_keys($allCountries)))
740       {
741         throw new InvalidArgumentException(sprintf('The following countries do not exist: %s.', implode(', ', $problems)));
742       }
743
744       $allCountries = array_intersect_key($allCountries, array_flip($countries));
745     }
746
747     $this->sortArray($allCountries);
748
749     return $allCountries;
750   }
751
752   /**
753    * Gets a list of currencies in the language of the localized version.
754    *
755    * @param  array   $currencies An array of currencies used to restrict the returned array (null by default, which means all currencies)
756    * @param  Boolean $full       Whether to return the symbol and the name or not (false by default)
757    *
758    * @return array a list of localized currencies.
759    */
760   public function getCurrencies($currencies = null, $full = false)
761   {
762     $allCurrencies = $this->findInfo('Currencies', true);
763
764     // restrict countries to a sub-set
765     if (null !== $currencies)
766     {
767       if ($problems = array_diff($currencies, array_keys($allCurrencies)))
768       {
769         throw new InvalidArgumentException(sprintf('The following currencies do not exist: %s.', implode(', ', $problems)));
770       }
771
772       $allCurrencies = array_intersect_key($allCurrencies, array_flip($currencies));
773     }
774
775     $tmp = array();
776     foreach ($allCurrencies as $key => $value)
777     {
778       $allCurrencies[$key] = $value[1];
779       $tmp[$key] = $value[0];
780     }
781
782     $this->sortArray($allCurrencies);
783
784     if ($full)
785     {
786         foreach ($allCurrencies as $key => $value)
787         {
788           $allCurrencies[$key] = array($tmp[$key], $value);
789         }
790     }
791
792     return $allCurrencies;
793   }
794
795   /**
796    * Gets a list of languages in the language of the localized version.
797    *
798    * @param  array $languages An array of languages used to restrict the returned array (null by default, which means all languages)
799    *
800    * @return array list of localized language names.
801    */
802   public function getLanguages($languages = null)
803   {
804     $allLanguages = $this->findInfo('Languages', true);
805
806     // restrict languages to a sub-set
807     if (null !== $languages)
808     {
809       if ($problems = array_diff($languages, array_keys($allLanguages)))
810       {
811         throw new InvalidArgumentException(sprintf('The following languages do not exist: %s.', implode(', ', $problems)));
812       }
813
814       $allLanguages = array_intersect_key($allLanguages, array_flip($languages));
815     }
816
817     $this->sortArray($allLanguages);
818
819     return $allLanguages;
820   }
821
822   /**
823    * Gets a list of scripts in the language of the localized version.
824    *
825    * @return array list of localized script names.
826    */
827   public function getScripts()
828   {
829     return $this->findInfo('Scripts', true);
830   }
831
832   /**
833    * Gets a list of timezones in the language of the localized version.
834    *
835    * @return array list of localized timezones.
836    */
837   public function getTimeZones()
838   {
839     //new format since ICU 3.8
840     //zoneStrings contains metaTimezones
841     $metadata = $this->findInfo('zoneStrings', true);
842     //TimeZones contains the Timezone name => metaTimezone identifier
843     $timeZones = $this->findInfo('TimeZones', true);
844     foreach ($timeZones as $key => $value)
845     {
846       $timeZones[$key] = $metadata['meta:'.$value];
847       $timeZones[$key]['identifier'] = $key;
848       $timeZones[$key]['city'] = str_replace('_', ' ', substr($key, strpos($key, '/') + 1));
849     }
850     return $timeZones;
851   }
852
853   /**
854    * sorts the passed array according to the locale of this sfCultureInfo class
855    *
856    * @param  array the array to be sorted with "asort" and this locale
857    */
858   public function sortArray(&$array)
859   {
860     $oldLocale = setlocale(LC_COLLATE, 0);
861     setlocale(LC_COLLATE, $this->getName());
862     asort($array, SORT_LOCALE_STRING);
863     setlocale(LC_COLLATE, $oldLocale);
864   }
865 }
866
Note: See TracBrowser for help on using the browser.