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:
parent
0b3a2c9a32
commit
c29db6270e
@ -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
|
||||
|
||||
|
112
src/RRule.php
112
src/RRule.php
@ -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' => '',
|
||||
|
@ -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]));
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user