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

Debug toolbar