1
0
mirror of https://github.com/rlanvin/php-rrule.git synced 2025-02-20 09:54:16 +01:00

Refactor i18nLoad to attept to solve #24

- Rework `RRule::i18nLoad()` to accept locales such as `en_sg` and use `Locale::parseLocale` when possible
- Fix `humanReadable` fails with `intl` enable when the timezone is "Z"
This commit is contained in:
rlanvin 2017-02-03 00:35:09 +02:00
parent 0b3a2c9a32
commit c29db6270e
3 changed files with 179 additions and 100 deletions

View File

@ -8,6 +8,8 @@
- Update exception message for UNTIL parse error [#23](https://github.com/rlanvin/php-rrule/pull/23)
- Fix parser handling of UNTIL when DTSTART is not provided [#25](https://github.com/rlanvin/php-rrule/issues/25)
- Accept invalid RFC strings generated by the JS lib but triggers a Notice message [#25](https://github.com/rlanvin/php-rrule/issues/25)
- Rework `RRule::i18nLoad()` to accept locales such as `en_sg` and use `Locale::parseLocale` when possible [#24](https://github.com/rlanvin/php-rrule/issues/24)
- Fix `humanReadable` fails with `intl` enable when the timezone is "Z" [#24](https://github.com/rlanvin/php-rrule/issues/24)
## [1.4.0] - 2016-11-11

View File

@ -2219,12 +2219,17 @@ class RRule implements RRuleInterface
);
///////////////////////////////////////////////////////////////////////////////
// i18n methods (could be moved into a separate class, since it's not always necessary)
// i18n methods
// these could be moved into a separate class maybe, since it's not always necessary
/**
* Stores translations once loaded (so we don't have to reload them all the time)
* @var array Stores translations once loaded (so we don't have to reload them all the time)
*/
static protected $i18n = array();
/**
* @var bool if intl extension is loaded
*/
static protected $intl_loaded = null;
/**
@ -2279,6 +2284,51 @@ class RRule implements RRuleInterface
}
}
/**
* Test if intl extension is loaded
* @return bool
*/
static public function intlLoaded()
{
if ( self::$intl_loaded === null ) {
self::$intl_loaded = extension_loaded('intl');
}
return self::$intl_loaded;
}
/**
* Parse a locale and returns a list of files to load.
*
* @return array
*/
static public function i18nFilesToLoad($locale, $use_intl = null)
{
if ( $use_intl === null ) {
$use_intl = self::intlLoaded();
}
$files = array();
if ( $use_intl ) {
$parsed = \Locale::parseLocale($locale);
$files[] = $parsed['language'];
if ( isset($parsed['region']) ) {
$files[] = $parsed['language'].'_'.$parsed['region'];
}
}
else {
if ( ! preg_match('/^([a-z]{2})(?:(?:_|-)[A-Z][a-z]+)?(?:(?:_|-)([A-Za-z]{2}))?(?:(?:_|-)[A-Z]*)?(?:\.[a-zA-Z\-0-9]*)?$/', $locale, $matches) ) {
throw new \InvalidArgumentException("The locale option does not look like a valid locale: $locale. For more option install the intl extension.");
}
$files[] = $matches[1];
if ( isset($matches[2]) ) {
$files[] = $matches[1].'_'.strtoupper($matches[2]);
}
}
return $files;
}
/**
* Load a translation file in memory.
* Will load the basic first (e.g. "en") and then the region-specific if any
@ -2291,17 +2341,9 @@ class RRule implements RRuleInterface
* @return array
* @throws \InvalidArgumentException
*/
static protected function i18nLoad($locale, $fallback = null)
static protected function i18nLoad($locale, $fallback = null, $use_intl = null)
{
if ( ! preg_match('/^([a-z]{2})(?:(?:_|-)[A-Z][a-z]+)?(?:(?:_|-)([A-Z]{2}))?(?:(?:_|-)[A-Z]*)?(?:\.[a-zA-Z\-0-9]*)?$/', $locale, $matches) ) {
throw new \InvalidArgumentException('The locale option does not look like a valid locale: '.$locale);
}
$files = array();
if ( isset($matches[2]) ) {
$files[] = $matches[1];
}
$files[] = $locale;
$files = self::i18nFilesToLoad($locale, $use_intl);
$result = array();
foreach ( $files as $file ) {
@ -2320,7 +2362,7 @@ class RRule implements RRuleInterface
if ( empty($result) ) {
if (!is_null($fallback)) {
return self::i18nLoad($fallback);
return self::i18nLoad($fallback, null, $use_intl);
}
throw new \RuntimeException("Failed to load translations for '$locale'");
}
@ -2338,27 +2380,28 @@ class RRule implements RRuleInterface
*/
public function humanReadable(array $opt = array())
{
if ( self::$intl_loaded === null ) {
self::$intl_loaded = extension_loaded('intl');
}
// attempt to detect default locale
if ( self::$intl_loaded ) {
$locale = \Locale::getDefault();
} else {
$locale = setlocale(LC_MESSAGES, 0);
if ($locale == 'C') {
$locale = 'en';
}
if ( ! isset($opt['use_intl']) ) {
$opt['use_intl'] = self::intlLoaded();
}
$default_opt = array(
'locale' => $locale,
'use_intl' => self::intlLoaded(),
'locale' => null,
'date_formatter' => null,
'fallback' => 'en',
);
if ( self::$intl_loaded ) {
// attempt to detect default locale
if ( $opt['use_intl'] ) {
$default_opt['locale'] = \Locale::getDefault();
} else {
$default_opt['locale'] = setlocale(LC_MESSAGES, 0);
if ( $default_opt['locale'] == 'C' ) {
$default_opt['locale'] = 'en';
}
}
if ( $opt['use_intl'] ) {
$default_opt['date_format'] = \IntlDateFormatter::SHORT;
if ( $this->freq >= self::SECONDLY || not_empty($this->rule['BYSECOND']) ) {
$default_opt['time_format'] = \IntlDateFormatter::LONG;
@ -2373,18 +2416,27 @@ class RRule implements RRuleInterface
$opt = array_merge($default_opt, $opt);
$i18n = self::i18nLoad($opt['locale'], $opt['fallback'], $opt['use_intl']);
if ( $opt['date_formatter'] && ! is_callable($opt['date_formatter']) ) {
throw new \InvalidArgumentException('The option date_formatter must callable');
}
if ( ! $opt['date_formatter'] ) {
if ( self::$intl_loaded ) {
if ( $opt['use_intl'] ) {
$timezone = $this->dtstart->getTimezone()->getName();
if ( $timezone == 'Z' ) {
$timezone = 'GMT'; // otherwise IntlDateFormatter::create fails because... reasons.
}
$formatter = \IntlDateFormatter::create(
$opt['locale'],
$opt['date_format'],
$opt['time_format'],
$this->dtstart->getTimezone()->getName()
$timezone
);
if ( ! $formatter ) {
throw new \RuntimeException('IntlDateFormatter::create() failed (this should not happen, please open a bug report!)');
}
$opt['date_formatter'] = function($date) use ($formatter) {
return $formatter->format($date);
};
@ -2396,8 +2448,6 @@ class RRule implements RRuleInterface
}
}
$i18n = self::i18nLoad($opt['locale'], $opt['fallback']);
$parts = array(
'freq' => '',
'byweekday' => '',

View File

@ -2404,24 +2404,28 @@ class RRuleTest extends PHPUnit_Framework_TestCase
///////////////////////////////////////////////////////////////////////////////
// Human readable string conversion
/**
* Providing a set of valid locales to call RRule::i18nLoad() with
*
* @return array
*/
public function validLocales()
{
return array(
// locale | result expected with intl | result expected without intl
// 2 characters language code
array('en'),
array('fr'),
array('en', array('en'), array('en')),
array('fr', array('fr'), array('fr')),
// with region and underscore
array('en_US'),
array('en_US.utf-8'),
array('en_US_POSIX'),
array('en_US', array('en','en_US'), array('en','en_US')),
array('en_US.utf-8', array('en','en_US'), array('en','en_US')),
array('en_US_POSIX', array('en','en_US'), array('en','en_US')),
// case insentitive
array('en_sg', array('en','en_SG'), array('en','en_SG')),
// with a dash
array('en-US'),
array('en-Hant-TW'), // real locale is zh-Hant-TW, but since we don't have a "zh" file, we just use en
array('en-US', array('en','en_US'), array('en','en_US')),
array('zh-Hant-TW', array('zh','zh_TW'), array('zh','zh_TW')), // real locale is zh-Hant-TW, but since we don't have a "zh" file, we just use "en" for the test
// invalid
array('eng', array('en'), false),
array('invalid', array('invalid'), false),
array('en_US._wr!ng', array('en','en_US'), false),
);
}
@ -2430,102 +2434,105 @@ class RRuleTest extends PHPUnit_Framework_TestCase
*
* @dataProvider validLocales
*/
public function testI18nLoad($locale)
public function testI18nFilesToLoadWithIntl($locale, $files)
{
$rrule = new RRule(array(
'freq' => 'daily',
'count' => 10,
'dtstart' => '2007-01-01'
));
if ( ! $files ) {
try {
$files = RRule::i18nFilesToLoad($locale, true);
$this->fail('Expected InvalidArgumentException not thrown (files was '.json_encode($files).')');
} catch (\InvalidArgumentException $e) {
}
}
else {
$this->assertEquals($files, RRule::i18nFilesToLoad($locale, true));
}
}
/**
* @dataProvider validLocales
*/
public function testI18nFilesToLoadWithoutIntl($locale, $dummy, $files)
{
if ( ! $files ) {
try {
RRule::i18nFilesToLoad($locale, false);
$this->fail('Expected InvalidArgumentException not thrown (files was '.json_encode($files).')');
} catch (\InvalidArgumentException $e) {
}
}
else {
$this->assertEquals($files, RRule::i18nFilesToLoad($locale, false));
}
}
/**
* Locales for which we have a translation
*/
public function validTranslatedLocales()
{
return array(
array('en'),
array('en_US')
);
}
/**
* Test that RRule::i18nLoad() does not throw an exception with valid locales.
*
* @dataProvider validTranslatedLocales
*/
public function testI18nLoadWithIntl($locale)
{
$reflector = new ReflectionClass('RRule\RRule');
$method = $reflector->getMethod('i18nLoad');
$method->setAccessible(true);
$result = $method->invokeArgs($rrule, array($locale));
$result = $method->invokeArgs(null, array($locale, null, true));
$this->assertNotEmpty($result);
}
/**
* Test that the RRule::i18nLoad() does not fail when provided with valid fallback locales
*
* @dataProvider validLocales
* @dataProvider validTranslatedLocales
*/
public function testI18nLoadFallback($fallback)
{
$date = date_create('2007-01-01');
$rrule = new RRule(array(
'freq' => 'daily',
'count' => 10,
'dtstart' => $date
));
$reflector = new ReflectionClass('RRule\RRule');
$method = $reflector->getMethod('i18nLoad');
$method->setAccessible(true);
// $result = $method->invokeArgs($rrule, array(array('locale' => 'xx', 'fallback' => $fallback)));
$result = $method->invokeArgs($rrule, array('xx', $fallback));
$result = $method->invokeArgs(null, array('xx', $fallback));
$this->assertNotEmpty($result);
}
/**
* Providing a set of invalid locales to call RRule::i18nLoad() with
*
* @return array
*/
public function invalidLocales()
{
return array(
array('eng'),
array('invalid'),
array('en_US._wr!ng'),
);
}
/**
* Tests that the RRule::i18nLoad() fails as expected on invalid $locale settings
*
* @dataProvider invalidLocales
* @expectedException \InvalidArgumentException
*/
public function testI18nLoadFails($locale)
public function testI18nLoadFailsWithoutIntl()
{
$date = date_create('2007-01-01');
$rrule = new RRule(array(
'freq' => 'daily',
'count' => 10,
'dtstart' => $date
));
$reflector = new ReflectionClass('RRule\RRule');
$method = $reflector->getMethod('i18nLoad');
$method->setAccessible(true);
$method->invokeArgs($rrule, array($locale, 'en')); // even with a valid fallback it should fail
$method->invokeArgs(null, array('invalid', 'en', false)); // even with a valid fallback it should fail
}
/**
* Tests that the RRule::i18nLoad() fails as expected on invalid $fallback settings
*
* @dataProvider invalidLocales
* @expectedException \InvalidArgumentException
*/
public function testI18nLoadFallbackFails($locale)
public function testI18nLoadFallbackFailsWitoutIntl()
{
$date = date_create('2007-01-01');
$rrule = new RRule(array(
'freq' => 'daily',
'count' => 10,
'dtstart' => $date
));
$reflector = new ReflectionClass('RRule\RRule');
$method = $reflector->getMethod('i18nLoad');
$method->setAccessible(true);
$method->invokeArgs($rrule, array('xx', $locale));
$method->invokeArgs(null, array('xx', 'invalid', false));
}
/**
@ -2547,7 +2554,7 @@ class RRuleTest extends PHPUnit_Framework_TestCase
/**
* Test that humanReadable works
*/
public function testHumanReadable()
public function testHumanReadableWithCLocale()
{
$rrule = new RRule(array(
'freq' => 'daily',
@ -2557,12 +2564,32 @@ class RRuleTest extends PHPUnit_Framework_TestCase
$reflector = new ReflectionClass('RRule\RRule');
// Force RRule::$intl_loaded to false, to test setlocale()
$property = $reflector->getProperty('intl_loaded');
$property->setAccessible(true);
$property->setValue($rrule, false);
setlocale(LC_MESSAGES, 'C');
$this->assertNotEmpty($rrule->humanReadable(array('fallback' => null)), 'C locale is converted to "en"');
}
public function humanReadableStrings()
{
return array(
array(
"DTSTART:20170202T000000Z\nFREQ=DAILY;UNTIL=20170205T000000Z",
"en",
"daily, starting from 2/2/17, until 2/5/17"
),
array(
"DTSTART:20170202T000000Z\nFREQ=DAILY;UNTIL=20170205T000000Z",
"en_IE",
"daily, starting from 02/02/2017, until 05/02/2017"
)
);
}
/**
* @dataProvider humanReadableStrings
*/
public function testHumanReadable($rrule,$locale, $string)
{
$rrule = new RRule($rrule);
$this->assertEquals($string, $rrule->humanReadable(['locale' => $locale]));
}
}