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