diff --git a/docs/en/changelogs/3.2.0.md b/docs/en/changelogs/3.2.0.md index cc17062c9..685663538 100644 --- a/docs/en/changelogs/3.2.0.md +++ b/docs/en/changelogs/3.2.0.md @@ -31,4 +31,15 @@ use `setDisplayFolderName()` with a folder path relative to `assets/`: UploadField::create('MyField')->setDisplayFolderName('Uploads'); +### Removed format detection in i18n::$date_format and i18n::$time_format + +Localized dates cause inconsistencies in client-side vs. server-side formatting +and validation, particularly in abbreviated month names. The default date +format has been changed to "yyyy-MM-dd" (e.g. 2014-12-31). +New users will continue to have the option for a localized date +format in their profile (based on their chosen locale). +If you have existing users with `Member.DateFormat` set to a format +including "MMM" or "MMMM", consider deleting those formats to fall back to +the global (and more stable) default. + ### Bugfixes diff --git a/docs/en/reference/datefield.md b/docs/en/reference/datefield.md index 57053c1b8..663aee840 100644 --- a/docs/en/reference/datefield.md +++ b/docs/en/reference/datefield.md @@ -37,6 +37,12 @@ You can define a custom dateformat for your Datefield based on [Zend_Date consta // will display a date in the following format: 31-06-2012 DateField::create('MyDate')->setConfig('dateformat', 'dd-MM-yyyy'); +Caution: If you're using natural language date formats like abbreviated month names +alongside the "showcalendar" option, you'll need to ensure the formats +between the calendar widget and the SilverStripe validation are consistent. +As an example for the 'de' locale, check `framework/thirdparty/jquery-ui/datepicker/i18n/jquery.ui.datepicker-de.js` +and compare it to `framework/thirdparty/Zend/Locale/Data/de.xml` +(see `` in the XML data). ## Min and Max Dates diff --git a/forms/DateField.php b/forms/DateField.php index f0bfdfa81..e198cce43 100644 --- a/forms/DateField.php +++ b/forms/DateField.php @@ -97,7 +97,7 @@ class DateField extends TextField { $this->config = $this->config()->default_config; if(!$this->getConfig('dateformat')) { - $this->setConfig('dateformat', i18n::get_date_format()); + $this->setConfig('dateformat', Config::inst()->get('i18n', 'date_format')); } foreach ($this->config()->default_config AS $defaultK => $defaultV) { diff --git a/forms/TimeField.php b/forms/TimeField.php index 173f34d79..6b9383e11 100644 --- a/forms/TimeField.php +++ b/forms/TimeField.php @@ -58,7 +58,7 @@ class TimeField extends TextField { $this->config = $this->config()->default_config; if(!$this->getConfig('timeformat')) { - $this->setConfig('timeformat', i18n::get_time_format()); + $this->setConfig('timeformat', Config::inst()->get('i18n', 'time_format')); } parent::__construct($name,$title,$value); diff --git a/i18n/i18n.php b/i18n/i18n.php index e0b121eda..bac113d21 100644 --- a/i18n/i18n.php +++ b/i18n/i18n.php @@ -84,13 +84,13 @@ class i18n extends Object implements TemplateGlobalProvider { * @config * @var string */ - private static $date_format; + private static $date_format = 'yyyy-MM-dd'; /** * @config * @var string */ - private static $time_format; + private static $time_format = 'H:mm'; /** * @var array Array of priority keys to instances of Zend_Translate, mapped by name. @@ -141,9 +141,8 @@ class i18n extends Object implements TemplateGlobalProvider { * @return string ISO date format */ public static function get_date_format() { - require_once 'Zend/Date.php'; - $dateFormat = Config::inst()->get('i18n', 'date_format'); - return ($dateFormat) ? $dateFormat : Zend_Locale_Format::getDateFormat(self::get_locale()); + Deprecation::notice('3.2', 'Use the "i18n.date_format" config setting instead'); + return Config::inst()->get('i18n', 'date_format'); } /** @@ -159,9 +158,8 @@ class i18n extends Object implements TemplateGlobalProvider { * @return string ISO time format */ public static function get_time_format() { - require_once 'Zend/Date.php'; - $timeFormat = Config::inst()->get('i18n', 'time_format'); - return ($timeFormat) ? $timeFormat : Zend_Locale_Format::getTimeFormat(self::get_locale()); + Deprecation::notice('3.2', 'Use the "i18n.time_format" config setting instead'); + return Config::inst()->get('i18n', 'time_format'); } /** diff --git a/security/Member.php b/security/Member.php index 4b58c92ad..7bd8f3a7a 100644 --- a/security/Member.php +++ b/security/Member.php @@ -1009,11 +1009,8 @@ class Member extends DataObject implements TemplateGlobalProvider { public function getDateFormat() { if($this->getField('DateFormat')) { return $this->getField('DateFormat'); - } elseif($this->getField('Locale')) { - require_once 'Zend/Date.php'; - return Zend_Locale_Format::getDateFormat($this->Locale); } else { - return i18n::get_date_format(); + return Config::inst()->get('i18n', 'date_format'); } } @@ -1027,11 +1024,8 @@ class Member extends DataObject implements TemplateGlobalProvider { public function getTimeFormat() { if($this->getField('TimeFormat')) { return $this->getField('TimeFormat'); - } elseif($this->getField('Locale')) { - require_once 'Zend/Date.php'; - return Zend_Locale_Format::getTimeFormat($this->Locale); } else { - return i18n::get_time_format(); + return Config::inst()->get('i18n', 'time_format'); } } @@ -1265,15 +1259,16 @@ class Member extends DataObject implements TemplateGlobalProvider { $permissionsTab = $fields->fieldByName("Root")->fieldByName('Permissions'); if($permissionsTab) $permissionsTab->addExtraClass('readonly'); - $defaultDateFormat = Zend_Locale_Format::getDateFormat(new Zend_Locale($this->Locale)); + $localDateFormat = Zend_Locale_Data::getContent(new Zend_Locale($this->Locale), 'date', 'short'); + // Ensure short dates always use four digit dates to avoid confusion + $localDateFormat = preg_replace('/(^|[^y])yy($|[^y])/', '$1yyyy$2', $localDateFormat); $dateFormatMap = array( - 'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'), + $this->DateFormat => Zend_Date::now()->toString($this->DateFormat), + $localDateFormat => Zend_Date::now()->toString($localDateFormat), 'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'), 'MM/dd/yyyy' => Zend_Date::now()->toString('MM/dd/yyyy'), 'dd/MM/yyyy' => Zend_Date::now()->toString('dd/MM/yyyy'), ); - $dateFormatMap[$defaultDateFormat] = Zend_Date::now()->toString($defaultDateFormat) - . sprintf(' (%s)', _t('Member.DefaultDateTime', 'default')); $mainFields->push( $dateFormatField = new MemberDatetimeOptionsetField( 'DateFormat', @@ -1283,13 +1278,13 @@ class Member extends DataObject implements TemplateGlobalProvider { ); $dateFormatField->setValue($this->DateFormat); - $defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($this->Locale)); + $localTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($this->Locale)); $timeFormatMap = array( + $this->TimeFormat => Zend_Date::now()->toString($this->TimeFormat), + $localTimeFormat => Zend_Date::now()->toString($localTimeFormat), 'h:mm a' => Zend_Date::now()->toString('h:mm a'), 'H:mm' => Zend_Date::now()->toString('H:mm'), ); - $timeFormatMap[$defaultTimeFormat] = Zend_Date::now()->toString($defaultTimeFormat) - . sprintf(' (%s)', _t('Member.DefaultDateTime', 'default')); $mainFields->push( $timeFormatField = new MemberDatetimeOptionsetField( 'TimeFormat', diff --git a/tests/forms/MemberDatetimeOptionsetFieldTest.php b/tests/forms/MemberDatetimeOptionsetFieldTest.php index ba6366836..5df684eda 100644 --- a/tests/forms/MemberDatetimeOptionsetFieldTest.php +++ b/tests/forms/MemberDatetimeOptionsetFieldTest.php @@ -11,7 +11,7 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest { require_once 'Zend/Date.php'; $defaultDateFormat = Zend_Locale_Format::getDateFormat($member->Locale); $dateFormatMap = array( - 'MMM d, yyyy' => Zend_Date::now()->toString('MMM d, yyyy'), + 'yyyy-MM-dd' => Zend_Date::now()->toString('yyyy-MM-dd'), 'yyyy/MM/dd' => Zend_Date::now()->toString('yyyy/MM/dd'), 'MM/dd/yyyy' => Zend_Date::now()->toString('MM/dd/yyyy'), 'dd/MM/yyyy' => Zend_Date::now()->toString('dd/MM/yyyy'), @@ -44,15 +44,17 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest { } public function testDateFormatDefaultCheckedInFormField() { + Config::inst()->update('i18n', 'date_format', 'yyyy-MM-dd'); $field = $this->createDateFormatFieldForMember($this->objFromFixture('Member', 'noformatmember')); $field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldList(), new FieldList())); // fake form $parser = new CSSContentParser($field->Field()); - $xmlArr = $parser->getBySelector('#Form_Form_DateFormat_MMM_d__y'); + $xmlArr = $parser->getBySelector('#Form_Form_DateFormat_yyyy-MM-dd'); $this->assertEquals('checked', (string) $xmlArr[0]['checked']); } public function testTimeFormatDefaultCheckedInFormField() { + Config::inst()->update('i18n', 'time_format', 'h:mm:ss a'); $field = $this->createTimeFormatFieldForMember($this->objFromFixture('Member', 'noformatmember')); $field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldList(), new FieldList())); // fake form diff --git a/tests/i18n/i18nTest.php b/tests/i18n/i18nTest.php index 1d2cc131c..c51e69c14 100644 --- a/tests/i18n/i18nTest.php +++ b/tests/i18n/i18nTest.php @@ -65,36 +65,6 @@ class i18nTest extends SapphireTest { parent::tearDown(); } - public function testDateFormatFromLocale() { - i18n::set_locale('en_US'); - $this->assertEquals('MMM d, y', i18n::get_date_format()); - i18n::set_locale('en_NZ'); - $this->assertEquals('d/MM/yyyy', i18n::get_date_format()); - i18n::set_locale('en_US'); - } - - public function testTimeFormatFromLocale() { - i18n::set_locale('en_US'); - $this->assertEquals('h:mm:ss a', i18n::get_time_format()); - i18n::set_locale('de_DE'); - $this->assertEquals('HH:mm:ss', i18n::get_time_format()); - i18n::set_locale('en_US'); - } - - public function testDateFormatCustom() { - i18n::set_locale('en_US'); - $this->assertEquals('MMM d, y', i18n::get_date_format()); - i18n::config()->date_format = 'd/MM/yyyy'; - $this->assertEquals('d/MM/yyyy', i18n::get_date_format()); - } - - public function testTimeFormatCustom() { - i18n::set_locale('en_US'); - $this->assertEquals('h:mm:ss a', i18n::get_time_format()); - i18n::config()->time_format = 'HH:mm:ss'; - $this->assertEquals('HH:mm:ss', i18n::get_time_format()); - } - public function testGetExistingTranslations() { $translations = i18n::get_existing_translations(); $this->assertTrue(isset($translations['en_US']), 'Checking for en translation'); diff --git a/tests/security/MemberTest.php b/tests/security/MemberTest.php index b137de4e3..b270c68f3 100644 --- a/tests/security/MemberTest.php +++ b/tests/security/MemberTest.php @@ -320,17 +320,13 @@ class MemberTest extends FunctionalTest { } public function testMemberWithNoDateFormatFallsbackToGlobalLocaleDefaultFormat() { + Config::inst()->update('i18n', 'date_format', 'yyyy-MM-dd'); + Config::inst()->update('i18n', 'time_format', 'H:mm'); $member = $this->objFromFixture('Member', 'noformatmember'); - $this->assertEquals('MMM d, y', $member->DateFormat); - $this->assertEquals('h:mm:ss a', $member->TimeFormat); + $this->assertEquals('yyyy-MM-dd', $member->DateFormat); + $this->assertEquals('H:mm', $member->TimeFormat); } - - public function testMemberWithNoDateFormatFallsbackToTheirLocaleDefaultFormat() { - $member = $this->objFromFixture('Member', 'delocalemember'); - $this->assertEquals('dd.MM.yyyy', $member->DateFormat); - $this->assertEquals('HH:mm:ss', $member->TimeFormat); - } - + public function testInGroups() { $staffmember = $this->objFromFixture('Member', 'staffmember'); $managementmember = $this->objFromFixture('Member', 'managementmember');