diff --git a/doc/changelog.txt b/doc/changelog.txt index e1a23423b8..c6ed3e108c 100644 --- a/doc/changelog.txt +++ b/doc/changelog.txt @@ -8,6 +8,7 @@ April 8th, 2013 4. Refactored title/meta rendering 5. Improved cache option files 6. Added Pi\Cache\Storage\Adapter\Filesystem to record file expiration so that TTL is not required any more for reading cache +7. Added support for Intl extension with intlDateFormatter and NumberFormatter suggested by @voltan in Issue #24 March 23rd, 2013 diff --git a/lib/Pi/Application/Resource/I18n.php b/lib/Pi/Application/Resource/I18n.php index 9360338f88..13c7f1f1af 100644 --- a/lib/Pi/Application/Resource/I18n.php +++ b/lib/Pi/Application/Resource/I18n.php @@ -42,11 +42,11 @@ public function boot() $locale = $locale ?: (isset($this->options['locale']) ? $this->options['locale'] : null); $charset = $charset ?: (isset($this->options['charset']) ? $this->options['charset'] : null); + // Set default locale Pi::service('i18n')->setLocale($locale); - //$locale = new Locale($locale, $charset); setlocale(LC_ALL, $locale); - //Pi::registry('locale', $locale); + // Preload translations if (!empty($this->options['translator'])) { $translator = Pi::service('i18n')->translator; if (!empty($this->options['translator']['global'])) { diff --git a/lib/Pi/Application/Service/I18n.php b/lib/Pi/Application/Service/I18n.php index f1edf8987e..b5ac00a969 100644 --- a/lib/Pi/Application/Service/I18n.php +++ b/lib/Pi/Application/Service/I18n.php @@ -137,6 +137,81 @@ * * __('A test message', 'theme/default', 'en'); * + * + * 7. Format a date + * + * if (!_intl()) date($format, $timestamp); // Skip if Intl extension is not available + * + * _date(time(), 'fa-IR', 'long', 'short', 'Asia/Tehran', 'persian'); + * _date(time(), array('locale' => 'fa-IR', 'datetype' => 'long', 'timetype' => 'short', 'timezone' => 'Asia/Tehran', 'calendar' => 'persian')); + * + * _date(time(), null, 'long', 'short', 'Asia/Tehran', 'persian'); + * _date(time(), array('datetype' => 'long', 'timetype' => 'short', 'timezone' => 'Asia/Tehran', 'calendar' => 'persian')); + * + * _date(time(), 'fa-IR@calendar=persian', 'long', 'short', 'Asia/Tehran'); + * _date(time(), array('locale' => 'fa-IR@calendar=persian', 'datetype' => 'long', 'timetype' => 'short', 'timezone' => 'Asia/Tehran')); + * + * _date(time(), null, null, null, null, 'persian'); + * _date(time(), array('calendar' => 'persian')); + * + * _date(time(), 'fa-IR', null, null, null, null, 'yyyy-MM-dd HH:mm:ss'); + * _date(time(), array('locale' => 'fa-IR', 'pattern' => 'yyyy-MM-dd HH:mm:ss')); + * + * _date(time(), null, null, null, null, null, 'yyyy-MM-dd HH:mm:ss'); + * _date(time(), array('pattern' => 'yyyy-MM-dd HH:mm:ss')); + * + * _date(time()); + * + * + * 8. Format a number + * + * if (!_intl()) return; // Skip if Intl extension is not available + * + * _number(123.4567, 'decimal', '#0.# kg', 'zh-CN', 'default'); + * _number(123.4567, 'decimal', '#0.# kg', 'zh-CN'); + * _number(123.4567, 'scientific'); + * _number(123.4567, 'spellout'); + * + * + * 9. Format a currency + * + * if (!_intl()) return; // Skip if Intl extension is not available + * + * _currency(123.45, 'USD', 'en-US'); + * _currency(123.45, 'USD'); + * _currency(123.45); + * + * + * 10. Get a date formatter + * + * if (!_intl()) return; // Skip if Intl extension is not available + * + * Pi::service('i18n')->getDateFormatter('fa-IR', 'long', 'short', 'Asia/Tehran', 'persian'); + * Pi::service('i18n')->getDateFormatter(array('locale' => 'fa-IR', 'datetype' => 'long', 'timetype' => 'short', 'timezone' => 'Asia/Tehran', 'calendar' => 'persian')); + * + * Pi::service('i18n')->getDateFormatter('fa-IR@calendar=persian', 'long', 'short', 'Asia/Tehran'); + * Pi::service('i18n')->getDateFormatter(array('locale' => 'fa-IR@calendar=persian', 'datetype' => 'long', 'timetype' => 'short', 'timezone' => 'Asia/Tehran')); + * + * _date(time(), 'fa-IR', null, null, null, null, 'yyyy-MM-dd HH:mm:ss'); + * _date(time(), array('locale' => 'fa-IR', 'pattern' => 'yyyy-MM-dd HH:mm:ss')); + * + * Pi::service('i18n')->getDateFormatter(null, null, null, null, null, 'yyyy-MM-dd HH:mm:ss'); + * Pi::service('i18n')->getDateFormatter(array('pattern' => 'yyyy-MM-dd HH:mm:ss')); + * + * + * 11. Get a number formatter + * + * if (!_intl()) return; // Skip if Intl extension is not available + * + * Pi::service('i18n')->getDateFormatter('decimal', '#0.# kg', 'zh-CN'); + * Pi::service('i18n')->getDateFormatter('decimal', '', 'zh-CN'); + * Pi::service('i18n')->getDateFormatter('decimal'); + * Pi::service('i18n')->getDateFormatter('scientific'); + * Pi::service('i18n')->getDateFormatter('spellout'); + * + * Pi::service('i18n')->getDateFormatter('currency', '', 'zh-CN'); + * Pi::service('i18n')->getDateFormatter('currency'); + * */ namespace Pi\Application\Service @@ -145,6 +220,15 @@ use Pi\I18n\Translator\LoaderPluginManager; use Pi\I18n\Translator\Translator; + /**#@+ + * Internationalization Functions + * @see http://www.php.net/manual/en/book.intl.php + */ + use IntlDateFormatter; + use NumberFormatter; + use Collator; + /**#@-*/ + class I18n extends AbstractService { //const NAMESPACE_GLOBAL = '_usr'; @@ -167,27 +251,6 @@ class I18n extends AbstractService */ protected $__translator; - /** - * \Collator - */ - protected $__collator; - /* - * \NumberFormatter - */ - protected $__numberFormatter; - /* - * \MessageFormatter - */ - protected $__messageFormatter; - /* - * \IntlDateFormatter - */ - protected $__dateFormatter; - /* - * \Transliterator - */ - protected $__transliterator; - /** * Get translator, instantiate it if not available * @@ -270,21 +333,19 @@ public function __get($name) case 'locale': return $this->getLocale(); break; - case 'collator': - if (!$this->__collator) { - $this->__collator = new \Collator($this->locale); - } - return $this->__collator; + case 'numberFormatter': + return $this->getNumberFormatter(); + break; + case 'dateFormatter': + return $this->getDateFormatter(); break; /**#@+ * @todo To implement the Intl extensions */ - case 'numberFormatter': + case 'collator': break; case 'messageFormatter': break; - case 'dateFormatter': - break; case 'transliterator': break; default: @@ -421,7 +482,98 @@ public function translate($message, $domain = null, $locale = null) return $this->getTranslator()->translate($message, $domain, $locale); } + + /** + * Load date formatter + * + * @see IntlDateFormatter + * + * @param array|string|null $locale + * @param string|null $datetype, valid values: 'NULL', 'FULL', 'LONG', 'MEDIUM', 'SHORT' + * @param string|null $timetype, valid values: 'NULL', 'FULL', 'LONG', 'MEDIUM', 'SHORT' + * @param string|null $timezone + * @param int|string|null $calendar + * @param string|null $pattern + * @return IntlDateFormatter + */ + public function getDateFormatter($locale = null, $datetype = null, $timetype = null, $timezone = null, $calendar = null, $pattern = null) + { + if (!class_exists('IntlDateFormatter')) { + return null; + } + + if (is_array($locale)) { + $params = $locale; + foreach (array('locale', 'datetype', 'timetype', 'timezone', 'calendar', 'pattern') as $key) { + ${$key} = isset($params[$key]) ? $params[$key] : null; + } + } + + if (!$locale) { + $locale = $this->getLocale(); + } elseif (strpos($locale, '@')) { + $calendar = IntlDateFormatter::TRADITIONAL; + } + + if (null === $calendar) { + $calendar = Pi::config('date_calendar', 'intl'); + if (!$calendar) { + $calendar = IntlDateFormatter::GREGORIAN; + } + } + if ($calendar && !is_numeric($calendar)) { + $locale .= '@calendar=' . $calendar; + $calendar = IntlDateFormatter::TRADITIONAL; + } + if (null === $calendar) { + $calendar = IntlDateFormatter::GREGORIAN; + } + + $datetype = constant('IntlDateFormatter::' . strtoupper($datetype ?: Pi::config('date_datetype', 'intl'))); + $timetype = constant('IntlDateFormatter::' . strtoupper($timetype ?: Pi::config('date_timetype', 'intl'))); + $timezone = $timezone ?: Pi::config('timezone'); + + $formatter = new IntlDateFormatter($locale, $datetype, $timetype, $timezone, $calendar); + + if (null === $pattern) { + $pattern = Pi::config('date_pattern', 'intl'); + } + if ($pattern) { + $formatter->setPattern($pattern); + } + + return $formatter; + } + + /** + * Load number formatter + * + * @see NumberFormatter + * + * @param string|null $style + * @param string|null $pattern + * @param string|null $locale + * @return NumberFormatter + */ + public function getNumberFormatter($style = null, $pattern = null, $locale = null) + { + if (!class_exists('NumberFormatter')) { + return null; + } + + $locale = $locale ?: $this->getLocale(); + $style = $style ?: Pi::config('number_style', 'intl'); + $style = $style ? constant('NumberFormatter::' . strtoupper($style)) : NumberFormatter::DEFAULT_STYLE; + $formatter = new NumberFormatter($locale, $style); + + if ($pattern) { + $formatter->setPattern($pattern); + } + + return $formatter; + } } + } /**#@+ @@ -453,5 +605,84 @@ function _e($message, $domain = null, $locale = null) { echo __($message, $domain, $locale); } + + /** + * Check if Intl functions are available + */ + function _intl() + { + return extension_loaded('intl') ? true : false; + } + + /** + * Locale-dependent formatting/parsing of date-time using pattern strings and/or canned patterns + * + * @param array|string|null $locale + * @param int|null $datetype + * @param int|null $timetype + * @param string|null $timezone + * @param int|string|null $calendar + * @param string|null $pattern + * @return string + */ + function _date($value, $locale = null, $datetype = null, $timetype = null, $timezone = null, $calendar = null, $pattern = null) + { + if (!_intl()) { + return false; + } + + $formatter = Pi::service('i18n')->getDateFormatter($locale, $datetype, $timetype, $timezone, $calendar, $pattern); + $result = $formatter->format($value); + + return $result; + } + + /** + * Locale-dependent formatting/parsing of number using pattern strings and/or canned patterns + * + * @param string|null $style + * @param string|null $pattern + * @param string|null $locale + * @param string|null $type + * @return mixed + */ + function _number($value, $style = null, $pattern = null, $locale = null, $type = null) + { + if (!_intl()) { + return false; + } + $formatter = Pi::service('i18n')->getNumberFormatter($style, $pattern, $locale); + if ($type) { + $type = constant('NumberFormatter::TYPE_' . strtoupper($type)); + $result = $formatter->format($value, $type); + } else { + $result = $formatter->format($value); + } + + return $result; + } + + /** + * Locale-dependent formatting/parsing of number using pattern strings and/or canned patterns + * + * @param string|null $currency + * @param string|null $locale + * @return string + */ + function _currency($value, $currency = null, $locale = null) + { + if (!_intl()) { + return false; + } + $result = $value; + $currency = (null === $currency) ? Pi::config('number_currency', 'intl') : $currency; + if ($currency) { + $style = 'CURRENCY'; + $formatter = Pi::service('i18n')->getNumberFormatter($style, $locale); + $result = $formatter->formatCurrency($value, $currency); + } + + return $result; + } } -/**#@-*/ +/**#@-*/ \ No newline at end of file diff --git a/usr/module/system/config/config.php b/usr/module/system/config/config.php index 85030d5d58..098278094c 100644 --- a/usr/module/system/config/config.php +++ b/usr/module/system/config/config.php @@ -132,6 +132,10 @@ 'name' => 'meta', 'title' => 'Head meta', ), + array( + 'name' => 'intl', + 'title' => 'Internationalization', + ), array( 'name' => 'mail', 'title' => 'Mailing', @@ -189,22 +193,6 @@ 'category' => 'general', ), - /* - 'timezone_server' => array( - 'title' => 'Server timezone', - 'description' => 'Timezone set by server.', - 'edit' => 'timezone', - 'category' => 'general', - ), - - 'timezone_system' => array( - 'title' => 'System timezone', - 'description' => 'Timezone for application system.', - 'edit' => 'timezone', - 'category' => 'general', - ), - */ - 'timezone' => array( 'title' => 'Timezone', 'description' => 'Timezone for application system.', @@ -249,6 +237,7 @@ ), // Meta section + 'copyright' => array( 'title' => 'Meta copyright', 'description' => 'The copyright meta tag defines any copyright statements you wish to disclose about your web page documents.', @@ -289,6 +278,102 @@ 'category' => 'meta', ), + // Internationalizaiton section + + 'number_style' => array( + 'title' => 'Default number style', + 'description' => 'See http://www.php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatstyle', + 'edit' => array( + 'type' => 'select', + 'options' => array( + 'options' => array( + 'DEFAULT_STYLE' => 'Default format for the locale', + 'PATTERN_DECIMAL' => 'Decimal format defined by pattern', + 'DECIMAL' => 'Decimal format', + 'PERCENT' => 'Percent format', + 'SCIENTIFIC' => 'Scientific format', + 'SPELLOUT' => 'Spellout rule-based format', + 'ORDINAL' => 'Ordinal rule-based format', + 'DURATION' => 'Duration rule-based format', + 'PATTERN_RULEBASED' => 'Rule-based format defined by pattern', + ), + ), + ), + 'value' => 'DEFAULT_STYLE', + 'category' => 'intl', + ), + + 'number_pattern' => array( + 'title' => 'Default pattern for selected number style', + 'description' => 'Only if required by style', + 'edit' => 'text', + 'value' => '', + 'category' => 'intl', + ), + + 'number_currency' => array( + 'title' => 'Default currency type', + 'description' => 'The 3-letter ISO 4217 currency code indicating the currency to use.', + 'edit' => 'text', + 'value' => '', + 'category' => 'intl', + ), + + 'date_calendar' => array( + 'title' => 'Default calendar for the locale', + 'description' => '"persian" is suggested for Persian language. See http://www.php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants.calendartypes', + 'edit' => 'text', + 'value' => '', + 'category' => 'intl', + ), + + 'date_datetype' => array( + 'title' => 'Default date type', + 'description' => 'See http://www.php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants', + 'edit' => array( + 'type' => 'select', + 'options' => array( + 'options' => array( + 'NONE' => 'Do not include this element', + 'FULL' => 'Completely specified style (Tuesday, April 12, 1952 AD or 3:30:42pm PST)', + 'LONG' => 'Long style (January 12, 1952 or 3:30:32pm)', + 'MEDIUM' => 'Medium style (Jan 12, 1952)', + 'SHORT' => 'Most abbreviated style, only essential data (12/13/52 or 3:30pm)', + ), + ), + ), + 'value' => 'MEDIUM', + 'category' => 'intl', + ), + + 'date_timetype' => array( + 'title' => 'Default time type', + 'description' => 'See http://www.php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants', + 'edit' => array( + 'type' => 'select', + 'options' => array( + 'options' => array( + 'NONE' => 'Do not include this element', + 'FULL' => 'Completely specified style (Tuesday, April 12, 1952 AD or 3:30:42pm PST)', + 'LONG' => 'Long style (January 12, 1952 or 3:30:32pm)', + 'MEDIUM' => 'Medium style (Jan 12, 1952)', + 'SHORT' => 'Most abbreviated style, only essential data (12/13/52 or 3:30pm)', + ), + ), + ), + 'value' => 'LONG', + 'category' => 'intl', + ), + + 'date_pattern' => array( + 'title' => 'Default formatting pattern for date-time', + 'description' => 'Be aware that both date type and time type are ignored if the pattern is set. See http://userguide.icu-project.org/formatparse/datetime', + 'edit' => 'text', + 'value' => 'yyyy-MM-dd HH:mm:ss', + 'category' => 'intl', + ), + + // Mailing section 'mailmethod' => array( diff --git a/usr/module/system/config/module.php b/usr/module/system/config/module.php index eaf9fd305a..f7123256af 100644 --- a/usr/module/system/config/module.php +++ b/usr/module/system/config/module.php @@ -29,7 +29,7 @@ // Description, for admin, optional 'description' => __('For administration of core functions of the site.'), // Version number, required - 'version' => '3.0.6', + 'version' => '3.1.0', // Distribution license, required 'license' => 'New BSD', // Logo image, for admin, optional diff --git a/usr/module/system/sql/mysql.system.sql b/usr/module/system/sql/mysql.system.sql index 1fc4b11897..0244e188e4 100644 --- a/usr/module/system/sql/mysql.system.sql +++ b/usr/module/system/sql/mysql.system.sql @@ -200,7 +200,7 @@ CREATE TABLE `{core.config}` ( `title` varchar(255) NOT NULL default '', `value` text, `description` varchar(255) NOT NULL default '', - `edit` tinytext default NULL, # callback options for edit + `edit` text, # callback options for edit `filter` varchar(64) NOT NULL default '', `order` smallint(5) unsigned NOT NULL default '0', `visible` tinyint(1) unsigned NOT NULL default '1', @@ -485,10 +485,10 @@ CREATE TABLE `{core.user_meta}` ( `title` varchar(255) NOT NULL default '', `attribute` varchar(255) default NULL, # profile column attribute `view` varchar(255) default NULL, # callback function for view - `edit` tinytext default NULL, # callback options for edit - `admin` tinytext default NULL, # callback options for administration - `search` tinytext default NULL, # callback options for search - `options` tinytext default NULL, # value options + `edit` text, # callback options for edit + `admin` text, # callback options for administration + `search` text, # callback options for search + `options` text, # value options `module` varchar(64) NOT NULL default '', `active` tinyint(1) NOT NULL default '0', `required` tinyint(1) NOT NULL default '0', diff --git a/usr/module/system/src/Installer/Action/Update.php b/usr/module/system/src/Installer/Action/Update.php index 9894173675..87ff8642a2 100644 --- a/usr/module/system/src/Installer/Action/Update.php +++ b/usr/module/system/src/Installer/Action/Update.php @@ -51,6 +51,45 @@ public function updateSchema(Event $e) { $moduleVersion = $e->getParam('version'); + if (version_compare($moduleVersion, '3.1.0', '<')): + + $sqlHandler = new SqlSchema; + $adapter = Pi::db()->getAdapter(); + + // Change fields from 'tinytext' to 'text' + $table = Pi::model('config')->getTable(); + $sql = sprintf('ALTER TABLE %s MODIFY `edit` text', $table); + try { + $adapter->query($sql, 'execute'); + } catch (\Exception $exception) { + $result = $e->getParam('result'); + $result['db'] = array( + 'status' => false, + 'message' => 'Table alter query failed: ' . $exception->getMessage(), + ); + $e->setParam('result', $result); + return false; + } + + $table = Pi::model('user_meta')->getTable(); + foreach (array('edit', 'admin', 'search', 'options') as $field) { + $sql = sprintf("ALTER TABLE %s MODIFY `{$field}` text", $table); + try { + $adapter->query($sql, 'execute'); + } catch (\Exception $exception) { + $result = $e->getParam('result'); + $result['db'] = array( + 'status' => false, + 'message' => 'Table alter query failed: ' . $exception->getMessage(), + ); + $e->setParam('result', $result); + return false; + } + } + + endif; + + if (version_compare($moduleVersion, '3.0.0-beta.3', '<')): // Add table of navigation data