From 200b923c9e38d48042865170fa5444f6e4303634 Mon Sep 17 00:00:00 2001 From: rlanvin Date: Sun, 13 Jan 2019 10:47:43 +0000 Subject: [PATCH] Add custom_path option to humanReadable If the option is present, it'll first look for a file in this folder before looking into the default folder. Fix #56 --- CHANGELOG.md | 4 ++ src/RRule.php | 45 +++++++++---- tests/RRuleTest.php | 34 ++++++++-- tests/i18n/fr_BE.php | 9 +++ tests/i18n/xx.php | 153 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 225 insertions(+), 20 deletions(-) create mode 100755 tests/i18n/fr_BE.php create mode 100755 tests/i18n/xx.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 17b7ddb..4abc5ea 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - Rewrite the core algorithm to use a native PHP generator, drop compability with PHP < 5.6 [#43](https://github.com/rlanvin/php-rrule/issues/43) +### Added + +- New option `custom_path` to `humanReadable()` to use custom translation files [#56](https://github.com/rlanvin/php-rrule/issues/56) + ## [1.6.3] - 2019-01-13 ### Fixed diff --git a/src/RRule.php b/src/RRule.php index 22bd86e..28e1dd9 100755 --- a/src/RRule.php +++ b/src/RRule.php @@ -2153,7 +2153,7 @@ class RRule implements RRuleInterface * Test if intl extension is loaded * @return bool */ - static public function intlLoaded() + static protected function intlLoaded() { if ( self::$intl_loaded === null ) { self::$intl_loaded = extension_loaded('intl'); @@ -2163,10 +2163,11 @@ class RRule implements RRuleInterface /** * Parse a locale and returns a list of files to load. + * For example "fr_FR" will produce "fr" and "fr_FR" * * @return array */ - static public function i18nFilesToLoad($locale, $use_intl = null) + static protected function i18nFilesToLoad($locale, $use_intl = null) { if ( $use_intl === null ) { $use_intl = self::intlLoaded(); @@ -2202,26 +2203,38 @@ class RRule implements RRuleInterface * * @param string $locale * @param string|null $fallback + * @param bool $use_intl + * @param string $custom_path * * @return array * @throws \InvalidArgumentException */ - static protected function i18nLoad($locale, $fallback = null, $use_intl = null) + static protected function i18nLoad($locale, $fallback = null, $use_intl = null, $custom_path = null) { $files = self::i18nFilesToLoad($locale, $use_intl); + $base_path = __DIR__.'/i18n'; + $result = array(); foreach ( $files as $file ) { - $path = __DIR__."/i18n/$file.php"; - if ( isset(self::$i18n[$file]) ) { - $result = array_merge($result, self::$i18n[$file]); - } - elseif ( is_file($path) && is_readable($path) ) { - self::$i18n[$file] = include $path; - $result = array_merge($result, self::$i18n[$file]); + + // if the file exists in $custom_path, it overrides the default + if ( $custom_path && is_file("$custom_path/$file.php") ) { + $path = "$custom_path/$file.php"; } else { - self::$i18n[$file] = array(); + $path = "$base_path/$file.php"; + } + + if ( isset(self::$i18n[$path]) ) { + $result = array_merge($result, self::$i18n[$path]); + } + elseif ( is_file($path) && is_readable($path) ) { + self::$i18n[$path] = include $path; + $result = array_merge($result, self::$i18n[$path]); + } + else { + self::$i18n[$path] = array(); } } @@ -2237,7 +2250,7 @@ class RRule implements RRuleInterface /** * Format a rule in a human readable string - * intl extension is required. + * `intl` extension is required. * * Available options * @@ -2249,6 +2262,9 @@ class RRule implements RRuleInterface * | `date_formatter` | callable| Function used to format the date (takes date, returns formatted) * | `explicit_inifite`| bool | Mention "forever" if the rule is infinite (true) * | `dtstart` | bool | Mention the start date (true) + * | `include_start` | bool | + * | `include_until` | bool | + * | `custom_path` | string | * * @param array $opt * @@ -2267,7 +2283,8 @@ class RRule implements RRuleInterface 'fallback' => 'en', 'explicit_infinite' => true, 'include_start' => true, - 'include_until' => true + 'include_until' => true, + 'custom_path' => null ); // attempt to detect default locale @@ -2295,7 +2312,7 @@ class RRule implements RRuleInterface $opt = array_merge($default_opt, $opt); - $i18n = self::i18nLoad($opt['locale'], $opt['fallback'], $opt['use_intl']); + $i18n = self::i18nLoad($opt['locale'], $opt['fallback'], $opt['use_intl'], $opt['custom_path']); if ( $opt['date_formatter'] && ! is_callable($opt['date_formatter']) ) { throw new \InvalidArgumentException('The option date_formatter must callable'); diff --git a/tests/RRuleTest.php b/tests/RRuleTest.php index 2a8ab85..9f90241 100755 --- a/tests/RRuleTest.php +++ b/tests/RRuleTest.php @@ -2629,15 +2629,19 @@ class RRuleTest extends TestCase */ public function testI18nFilesToLoadWithIntl($locale, $files) { + $reflector = new ReflectionClass('RRule\RRule'); + $method = $reflector->getMethod('i18nFilesToLoad'); + $method->setAccessible(true); + if ( ! $files ) { try { - $files = RRule::i18nFilesToLoad($locale, true); + $method->invokeArgs(null, array($locale, true)); $this->fail('Expected InvalidArgumentException not thrown (files was '.json_encode($files).')'); } catch (\InvalidArgumentException $e) { } } else { - $this->assertEquals($files, RRule::i18nFilesToLoad($locale, true)); + $this->assertEquals($files,$method->invokeArgs(null, array($locale, true))); } } @@ -2646,15 +2650,19 @@ class RRuleTest extends TestCase */ public function testI18nFilesToLoadWithoutIntl($locale, $dummy, $files) { + $reflector = new ReflectionClass('RRule\RRule'); + $method = $reflector->getMethod('i18nFilesToLoad'); + $method->setAccessible(true); + if ( ! $files ) { try { - RRule::i18nFilesToLoad($locale, false); + $method->invokeArgs(null, array($locale, false)); $this->fail('Expected InvalidArgumentException not thrown (files was '.json_encode($files).')'); } catch (\InvalidArgumentException $e) { } } else { - $this->assertEquals($files, RRule::i18nFilesToLoad($locale, false)); + $this->assertEquals($files, $method->invokeArgs(null, array($locale, false))); } } @@ -2755,8 +2763,6 @@ class RRuleTest extends TestCase 'dtstart' => '2007-01-01' )); - $reflector = new ReflectionClass('RRule\RRule'); - setlocale(LC_MESSAGES, 'C'); $this->assertNotEmpty($rrule->humanReadable(array('fallback' => null)), 'C locale is converted to "en"'); } @@ -2805,6 +2811,22 @@ class RRuleTest extends TestCase array('locale' => "en_IE", 'include_start' => false, 'explicit_infinite' => false), "daily" ), + // with custom_path + 'custom_path' => array( + "DTSTART:20170202T000000Z\nRRULE:FREQ=YEARLY;UNTIL=20170205T000000Z", + array('locale' => "fr_BE", "custom_path" => __DIR__."/i18n"), + "chaque année, à partir du 2/02/17, jusqu'au 5/02/17" + ), + 'custom_path cached separately' => array( + "DTSTART:20170202T000000Z\nRRULE:FREQ=YEARLY;UNTIL=20170205T000000Z", + array('locale' => "fr_BE"), + "tous les ans, à partir du 2/02/17, jusqu'au 5/02/17", + ), + array( + "RRULE:FREQ=DAILY;UNTIL=20190405T055959Z", + array('locale' => "xx", "custom_path" => __DIR__."/i18n", "date_formatter" => function($date) { return "X"; }), + "daily, starting from X, until X" + ), ); } diff --git a/tests/i18n/fr_BE.php b/tests/i18n/fr_BE.php new file mode 100755 index 0000000..54394c7 --- /dev/null +++ b/tests/i18n/fr_BE.php @@ -0,0 +1,9 @@ + array( + '1' => 'chaque année', + '2' => 'une années sur deux', + 'else' => 'toutes les %{interval} années' + ) +); \ No newline at end of file diff --git a/tests/i18n/xx.php b/tests/i18n/xx.php new file mode 100755 index 0000000..e21e231 --- /dev/null +++ b/tests/i18n/xx.php @@ -0,0 +1,153 @@ + array( + '1' => 'yearly', + 'else' => 'every %{interval} years' + ), + 'monthly' => array( + '1' => 'monthly', + 'else' => 'every %{interval} months' + ), + 'weekly' => array( + '1' => 'weekly', + '2' => 'every other week', + 'else' => 'every %{interval} weeks' + ), + 'daily' => array( + '1' => 'daily', + '2' => 'every other day', + 'else' => 'every %{interval} days' + ), + 'hourly' => array( + '1' => 'hourly', + 'else' => 'every %{interval} hours' + ), + 'minutely' => array( + '1' => 'minutely', + 'else' => 'every %{interval} minutes' + ), + 'secondly' => array( + '1' => 'secondly', + 'else' => 'every %{interval} seconds' + ), + 'dtstart' => ', starting from %{date}', + 'infinite' => ', forever', + 'until' => ', until %{date}', + 'count' => array( + '1' => ', one time', + 'else' => ', %{count} times' + ), + 'and' => 'and', + 'x_of_the_y' => array( + 'yearly' => '%{x} of the year', // e.g. the first Monday of the year, or the first day of the year + 'monthly' => '%{x} of the month', + ), + 'bymonth' => ' in %{months}', + 'months' => array( + 1 => 'January', + 2 => 'February', + 3 => 'March', + 4 => 'April', + 5 => 'May', + 6 => 'June', + 7 => 'July', + 8 => 'August', + 9 => 'September', + 10 => 'October', + 11 => 'November', + 12 => 'December', + ), + 'byweekday' => ' on %{weekdays}', + 'weekdays' => array( + 1 => 'Monday', + 2 => 'Tuesday', + 3 => 'Wednesday', + 4 => 'Thursday', + 5 => 'Friday', + 6 => 'Saturday', + 7 => 'Sunday', + ), + 'nth_weekday' => array( + '1' => 'the first %{weekday}', // e.g. the first Monday + '2' => 'the second %{weekday}', + '3' => 'the third %{weekday}', + 'else' => 'the %{n}th %{weekday}' + ), + '-nth_weekday' => array( + '-1' => 'the last %{weekday}', // e.g. the last Monday + '-2' => 'the penultimate %{weekday}', + '-3' => 'the antepenultimate %{weekday}', + 'else' => 'the %{n}th to the last %{weekday}' + ), + 'byweekno' => array( + '1' => ' on week %{weeks}', + 'else' => ' on weeks number %{weeks}' + ), + 'nth_weekno' => '%{n}', + 'bymonthday' => ' on %{monthdays}', + 'nth_monthday' => array( + '1' => 'the 1st', + '2' => 'the 2nd', + '3' => 'the 3rd', + '21' => 'the 21st', + '22' => 'the 22nd', + '23' => 'the 23rd', + '31' => 'the 31st', + 'else' => 'the %{n}th' + ), + '-nth_monthday' => array( + '-1' => 'the last day', + '-2' => 'the penultimate day', + '-3' => 'the antepenultimate day', + '-21' => 'the 21st to the last day', + '-22' => 'the 22nd to the last day', + '-23' => 'the 23rd to the last day', + '-31' => 'the 31st to the last day', + 'else' => 'the %{n}th to the last day' + ), + 'byyearday' => array( + '1' => ' on %{yeardays} day', + 'else' => ' on %{yeardays} days' + ), + 'nth_yearday' => array( + '1' => 'the first', + '2' => 'the second', + '3' => 'the third', + 'else' => 'the %{n}th' + ), + '-nth_yearday' => array( + '-1' => 'the last', + '-2' => 'the penultimate', + '-3' => 'the antepenultimate', + 'else' => 'the %{n}th to the last' + ), + 'byhour' => array( + '1' => ' at %{hours}', + 'else' => ' at %{hours}' + ), + 'nth_hour' => '%{n}h', + 'byminute' => array( + '1' => ' at minute %{minutes}', + 'else' => ' at minutes %{minutes}' + ), + 'nth_minute' => '%{n}', + 'bysecond' => array( + '1' => ' at second %{seconds}', + 'else' => ' at seconds %{seconds}' + ), + 'nth_second' => '%{n}', + 'bysetpos' => ', but only %{setpos} instance of this set', + 'nth_setpos' => array( + '1' => 'the first', + '2' => 'the second', + '3' => 'the third', + 'else' => 'the %{n}th' + ), + '-nth_setpos' => array( + '-1' => 'the last', + '-2' => 'the penultimate', + '-3' => 'the antepenultimate', + 'else' => 'the %{n}th to the last' + ) +); \ No newline at end of file