diff options
author | ArthurHoaro <arthur@hoa.ro> | 2017-03-06 21:13:48 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-03-06 21:13:48 +0100 |
commit | 9971f7c82cc47446b457464cec5b4fefcae470e1 (patch) | |
tree | 87b01fc1dd9d0ca1f0c874aad847029eab74d0d8 | |
parent | 236239be752a7bb24547237b5751ac4fcbc0e549 (diff) | |
parent | 36c8fb1ef869c29e783f0dd5ebef2fb5566e2611 (diff) | |
download | Shaarli-9971f7c82cc47446b457464cec5b4fefcae470e1.tar.gz Shaarli-9971f7c82cc47446b457464cec5b4fefcae470e1.tar.zst Shaarli-9971f7c82cc47446b457464cec5b4fefcae470e1.zip |
Merge pull request #750 from ArthurHoaro/feature/intl-dates
Improve datetime display
-rw-r--r-- | .travis.yml | 8 | ||||
-rw-r--r-- | Makefile | 16 | ||||
-rw-r--r-- | application/Utils.php | 85 | ||||
-rw-r--r-- | composer.json | 3 | ||||
-rw-r--r-- | phpunit.xml | 20 | ||||
-rw-r--r-- | tests/UtilsTest.php | 46 | ||||
-rw-r--r-- | tests/languages/bootstrap.php | 7 | ||||
-rw-r--r-- | tests/languages/de/UtilsDeTest.php | 25 | ||||
-rw-r--r-- | tests/languages/en/UtilsEnTest.php | 25 | ||||
-rw-r--r-- | tests/languages/fr/UtilsFrTest.php | 25 | ||||
-rw-r--r-- | tpl/default/linklist.html | 5 | ||||
-rw-r--r-- | tpl/vintage/linklist.html | 4 |
12 files changed, 246 insertions, 23 deletions
diff --git a/.travis.yml b/.travis.yml index 03071a47..59b86c08 100644 --- a/.travis.yml +++ b/.travis.yml | |||
@@ -1,5 +1,11 @@ | |||
1 | sudo: false | 1 | sudo: false |
2 | language: php | 2 | language: php |
3 | addons: | ||
4 | apt: | ||
5 | packages: | ||
6 | - locales | ||
7 | - language-pack-de | ||
8 | - language-pack-fr | ||
3 | cache: | 9 | cache: |
4 | directories: | 10 | directories: |
5 | - $HOME/.composer/cache | 11 | - $HOME/.composer/cache |
@@ -14,4 +20,4 @@ install: | |||
14 | script: | 20 | script: |
15 | - make clean | 21 | - make clean |
16 | - make check_permissions | 22 | - make check_permissions |
17 | - make test | 23 | - make all_tests |
@@ -124,8 +124,20 @@ test: | |||
124 | @echo "-------" | 124 | @echo "-------" |
125 | @echo "PHPUNIT" | 125 | @echo "PHPUNIT" |
126 | @echo "-------" | 126 | @echo "-------" |
127 | @mkdir -p sandbox | 127 | @mkdir -p sandbox coverage |
128 | @$(BIN)/phpunit tests | 128 | @$(BIN)/phpunit --coverage-php coverage/main.cov --testsuite unit-tests |
129 | |||
130 | locale_test_%: | ||
131 | @UT_LOCALE=$*.utf8 \ | ||
132 | $(BIN)/phpunit \ | ||
133 | --coverage-php coverage/$(firstword $(subst _, ,$*)).cov \ | ||
134 | --bootstrap tests/languages/bootstrap.php \ | ||
135 | --testsuite language-$(firstword $(subst _, ,$*)) | ||
136 | |||
137 | all_tests: test locale_test_de_DE locale_test_en_US locale_test_fr_FR | ||
138 | @$(BIN)/phpcov merge --html coverage coverage | ||
139 | @# --text doesn't work with phpunit 4.* (v5 requires PHP 5.6) | ||
140 | @#$(BIN)/phpcov merge --text coverage/txt coverage | ||
129 | 141 | ||
130 | ## | 142 | ## |
131 | # Custom release archive generation | 143 | # Custom release archive generation |
diff --git a/application/Utils.php b/application/Utils.php index 35d65224..a936b09f 100644 --- a/application/Utils.php +++ b/application/Utils.php | |||
@@ -216,20 +216,55 @@ function is_session_id_valid($sessionId) | |||
216 | function autoLocale($headerLocale) | 216 | function autoLocale($headerLocale) |
217 | { | 217 | { |
218 | // Default if browser does not send HTTP_ACCEPT_LANGUAGE | 218 | // Default if browser does not send HTTP_ACCEPT_LANGUAGE |
219 | $attempts = array('en_US'); | 219 | $attempts = array('en_US', 'en_US.utf8', 'en_US.UTF-8'); |
220 | if (isset($headerLocale)) { | 220 | if (isset($headerLocale)) { |
221 | // (It's a bit crude, but it works very well. Preferred language is always presented first.) | 221 | // (It's a bit crude, but it works very well. Preferred language is always presented first.) |
222 | if (preg_match('/([a-z]{2})-?([a-z]{2})?/i', $headerLocale, $matches)) { | 222 | if (preg_match('/([a-z]{2,3})[-_]?([a-z]{2})?/i', $headerLocale, $matches)) { |
223 | $loc = $matches[1] . (!empty($matches[2]) ? '_' . strtoupper($matches[2]) : ''); | 223 | $first = [strtolower($matches[1]), strtoupper($matches[1])]; |
224 | $attempts = array( | 224 | $separators = ['_', '-']; |
225 | $loc.'.UTF-8', $loc, str_replace('_', '-', $loc).'.UTF-8', str_replace('_', '-', $loc), | 225 | $encodings = ['utf8', 'UTF-8']; |
226 | $loc . '_' . strtoupper($loc).'.UTF-8', $loc . '_' . strtoupper($loc), | 226 | if (!empty($matches[2])) { |
227 | $loc . '_' . $loc.'.UTF-8', $loc . '_' . $loc, $loc . '-' . strtoupper($loc).'.UTF-8', | 227 | $second = [strtoupper($matches[2]), strtolower($matches[2])]; |
228 | $loc . '-' . strtoupper($loc), $loc . '-' . $loc.'.UTF-8', $loc . '-' . $loc | 228 | $attempts = cartesian_product_generator([$first, $separators, $second, ['.'], $encodings]); |
229 | ); | 229 | } else { |
230 | $attempts = cartesian_product_generator([$first, $separators, $first, ['.'], $encodings]); | ||
231 | } | ||
232 | } | ||
233 | } | ||
234 | setlocale(LC_ALL, implode('implode', iterator_to_array($attempts))); | ||
235 | } | ||
236 | |||
237 | /** | ||
238 | * Build a Generator object representing the cartesian product from given $items. | ||
239 | * | ||
240 | * Example: | ||
241 | * [['a'], ['b', 'c']] | ||
242 | * will generate: | ||
243 | * [ | ||
244 | * ['a', 'b'], | ||
245 | * ['a', 'c'], | ||
246 | * ] | ||
247 | * | ||
248 | * @param array $items array of array of string | ||
249 | * | ||
250 | * @return Generator representing the cartesian product of given array. | ||
251 | * | ||
252 | * @see https://en.wikipedia.org/wiki/Cartesian_product | ||
253 | */ | ||
254 | function cartesian_product_generator($items) | ||
255 | { | ||
256 | if (empty($items)) { | ||
257 | yield []; | ||
258 | } | ||
259 | $subArray = array_pop($items); | ||
260 | if (empty($subArray)) { | ||
261 | return; | ||
262 | } | ||
263 | foreach (cartesian_product_generator($items) as $item) { | ||
264 | foreach ($subArray as $value) { | ||
265 | yield $item + [count($item) => $value]; | ||
230 | } | 266 | } |
231 | } | 267 | } |
232 | setlocale(LC_ALL, $attempts); | ||
233 | } | 268 | } |
234 | 269 | ||
235 | /** | 270 | /** |
@@ -270,3 +305,33 @@ function normalize_spaces($string) | |||
270 | { | 305 | { |
271 | return preg_replace('/\s{2,}/', ' ', trim($string)); | 306 | return preg_replace('/\s{2,}/', ' ', trim($string)); |
272 | } | 307 | } |
308 | |||
309 | /** | ||
310 | * Format the date according to the locale. | ||
311 | * | ||
312 | * Requires php-intl to display international datetimes, | ||
313 | * otherwise default format '%c' will be returned. | ||
314 | * | ||
315 | * @param DateTime $date to format. | ||
316 | * @param bool $intl Use international format if true. | ||
317 | * | ||
318 | * @return bool|string Formatted date, or false if the input is invalid. | ||
319 | */ | ||
320 | function format_date($date, $intl = true) | ||
321 | { | ||
322 | if (! $date instanceof DateTime) { | ||
323 | return false; | ||
324 | } | ||
325 | |||
326 | if (! $intl || ! class_exists('IntlDateFormatter')) { | ||
327 | return strftime('%c', $date->getTimestamp()); | ||
328 | } | ||
329 | |||
330 | $formatter = new IntlDateFormatter( | ||
331 | setlocale(LC_TIME, 0), | ||
332 | IntlDateFormatter::LONG, | ||
333 | IntlDateFormatter::LONG | ||
334 | ); | ||
335 | |||
336 | return $formatter->format($date); | ||
337 | } | ||
diff --git a/composer.json b/composer.json index b82aceef..70b87bb9 100644 --- a/composer.json +++ b/composer.json | |||
@@ -20,7 +20,8 @@ | |||
20 | "phpmd/phpmd" : "@stable", | 20 | "phpmd/phpmd" : "@stable", |
21 | "phpunit/phpunit": "4.8.*", | 21 | "phpunit/phpunit": "4.8.*", |
22 | "sebastian/phpcpd": "*", | 22 | "sebastian/phpcpd": "*", |
23 | "squizlabs/php_codesniffer": "2.*" | 23 | "squizlabs/php_codesniffer": "2.*", |
24 | "phpunit/phpcov": "*" | ||
24 | }, | 25 | }, |
25 | "autoload": { | 26 | "autoload": { |
26 | "psr-4": { | 27 | "psr-4": { |
diff --git a/phpunit.xml b/phpunit.xml index d6e01c35..8b66e6c5 100644 --- a/phpunit.xml +++ b/phpunit.xml | |||
@@ -3,13 +3,25 @@ | |||
3 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | 3 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
4 | xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.5/phpunit.xsd" | 4 | xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.5/phpunit.xsd" |
5 | colors="true"> | 5 | colors="true"> |
6 | <testsuites> | ||
7 | <testsuite name="unit-tests"> | ||
8 | <directory>tests</directory> | ||
9 | <exclude>tests/languages</exclude> | ||
10 | </testsuite> | ||
11 | <testsuite name="language-de"> | ||
12 | <directory>tests/languages/de</directory> | ||
13 | </testsuite> | ||
14 | <testsuite name="language-en"> | ||
15 | <directory>tests/languages/en</directory> | ||
16 | </testsuite> | ||
17 | <testsuite name="language-fr"> | ||
18 | <directory>tests/languages/fr</directory> | ||
19 | </testsuite> | ||
20 | </testsuites> | ||
21 | |||
6 | <filter> | 22 | <filter> |
7 | <whitelist addUncoveredFilesFromWhitelist="true"> | 23 | <whitelist addUncoveredFilesFromWhitelist="true"> |
8 | <directory suffix=".php">application</directory> | 24 | <directory suffix=".php">application</directory> |
9 | </whitelist> | 25 | </whitelist> |
10 | </filter> | 26 | </filter> |
11 | <logging> | ||
12 | <log type="coverage-html" target="coverage" lowUpperBound="30" highLowerBound="80"/> | ||
13 | <log type="coverage-text" target="php://stdout" showUncoveredFiles="true"/> | ||
14 | </logging> | ||
15 | </phpunit> | 27 | </phpunit> |
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index c885f552..e70cc1ae 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php | |||
@@ -23,7 +23,12 @@ class UtilsTest extends PHPUnit_Framework_TestCase | |||
23 | 23 | ||
24 | // Expected log date format | 24 | // Expected log date format |
25 | protected static $dateFormat = 'Y/m/d H:i:s'; | 25 | protected static $dateFormat = 'Y/m/d H:i:s'; |
26 | 26 | ||
27 | /** | ||
28 | * @var string Save the current timezone. | ||
29 | */ | ||
30 | protected static $defaultTimeZone; | ||
31 | |||
27 | 32 | ||
28 | /** | 33 | /** |
29 | * Assign reference data | 34 | * Assign reference data |
@@ -31,6 +36,17 @@ class UtilsTest extends PHPUnit_Framework_TestCase | |||
31 | public static function setUpBeforeClass() | 36 | public static function setUpBeforeClass() |
32 | { | 37 | { |
33 | self::$sidHashes = ReferenceSessionIdHashes::getHashes(); | 38 | self::$sidHashes = ReferenceSessionIdHashes::getHashes(); |
39 | self::$defaultTimeZone = date_default_timezone_get(); | ||
40 | // Timezone without DST for test consistency | ||
41 | date_default_timezone_set('Africa/Nairobi'); | ||
42 | } | ||
43 | |||
44 | /** | ||
45 | * Reset the timezone | ||
46 | */ | ||
47 | public static function tearDownAfterClass() | ||
48 | { | ||
49 | date_default_timezone_set(self::$defaultTimeZone); | ||
34 | } | 50 | } |
35 | 51 | ||
36 | /** | 52 | /** |
@@ -282,4 +298,32 @@ class UtilsTest extends PHPUnit_Framework_TestCase | |||
282 | $this->assertEquals('', normalize_spaces('')); | 298 | $this->assertEquals('', normalize_spaces('')); |
283 | $this->assertEquals(null, normalize_spaces(null)); | 299 | $this->assertEquals(null, normalize_spaces(null)); |
284 | } | 300 | } |
301 | |||
302 | /** | ||
303 | * Test arrays_combine | ||
304 | */ | ||
305 | public function testCartesianProductGenerator() | ||
306 | { | ||
307 | $arr = [['ab', 'cd'], ['ef', 'gh'], ['ij', 'kl'], ['m']]; | ||
308 | $expected = [ | ||
309 | ['ab', 'ef', 'ij', 'm'], | ||
310 | ['ab', 'ef', 'kl', 'm'], | ||
311 | ['ab', 'gh', 'ij', 'm'], | ||
312 | ['ab', 'gh', 'kl', 'm'], | ||
313 | ['cd', 'ef', 'ij', 'm'], | ||
314 | ['cd', 'ef', 'kl', 'm'], | ||
315 | ['cd', 'gh', 'ij', 'm'], | ||
316 | ['cd', 'gh', 'kl', 'm'], | ||
317 | ]; | ||
318 | $this->assertEquals($expected, iterator_to_array(cartesian_product_generator($arr))); | ||
319 | } | ||
320 | |||
321 | /** | ||
322 | * Test date_format() with invalid parameter. | ||
323 | */ | ||
324 | public function testDateFormatInvalid() | ||
325 | { | ||
326 | $this->assertFalse(format_date([])); | ||
327 | $this->assertFalse(format_date(null)); | ||
328 | } | ||
285 | } | 329 | } |
diff --git a/tests/languages/bootstrap.php b/tests/languages/bootstrap.php new file mode 100644 index 00000000..95609210 --- /dev/null +++ b/tests/languages/bootstrap.php | |||
@@ -0,0 +1,7 @@ | |||
1 | <?php | ||
2 | if (! empty('UT_LOCALE')) { | ||
3 | setlocale(LC_ALL, getenv('UT_LOCALE')); | ||
4 | } | ||
5 | |||
6 | require_once 'vendor/autoload.php'; | ||
7 | |||
diff --git a/tests/languages/de/UtilsDeTest.php b/tests/languages/de/UtilsDeTest.php new file mode 100644 index 00000000..8a740389 --- /dev/null +++ b/tests/languages/de/UtilsDeTest.php | |||
@@ -0,0 +1,25 @@ | |||
1 | <?php | ||
2 | |||
3 | require_once 'tests/UtilsTest.php'; | ||
4 | |||
5 | |||
6 | class UtilsDeTest extends UtilsTest | ||
7 | { | ||
8 | /** | ||
9 | * Test date_format(). | ||
10 | */ | ||
11 | public function testDateFormat() | ||
12 | { | ||
13 | $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); | ||
14 | $this->assertRegExp('/1. Januar 2017 (um )?10:11:12 GMT\+0?3(:00)?/', format_date($date, true)); | ||
15 | } | ||
16 | |||
17 | /** | ||
18 | * Test date_format() using builtin PHP function strftime. | ||
19 | */ | ||
20 | public function testDateFormatDefault() | ||
21 | { | ||
22 | $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); | ||
23 | $this->assertEquals('So 01 Jan 2017 10:11:12 EAT', format_date($date, false)); | ||
24 | } | ||
25 | } | ||
diff --git a/tests/languages/en/UtilsEnTest.php b/tests/languages/en/UtilsEnTest.php new file mode 100644 index 00000000..60bcb653 --- /dev/null +++ b/tests/languages/en/UtilsEnTest.php | |||
@@ -0,0 +1,25 @@ | |||
1 | <?php | ||
2 | |||
3 | require_once 'tests/UtilsTest.php'; | ||
4 | |||
5 | |||
6 | class UtilsEnTest extends UtilsTest | ||
7 | { | ||
8 | /** | ||
9 | * Test date_format(). | ||
10 | */ | ||
11 | public function testDateFormat() | ||
12 | { | ||
13 | $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); | ||
14 | $this->assertRegExp('/January 1, 2017 (at )?10:11:12 AM GMT\+0?3(:00)?/', format_date($date, true)); | ||
15 | } | ||
16 | |||
17 | /** | ||
18 | * Test date_format() using builtin PHP function strftime. | ||
19 | */ | ||
20 | public function testDateFormatDefault() | ||
21 | { | ||
22 | $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); | ||
23 | $this->assertEquals('Sun 01 Jan 2017 10:11:12 AM EAT', format_date($date, false)); | ||
24 | } | ||
25 | } | ||
diff --git a/tests/languages/fr/UtilsFrTest.php b/tests/languages/fr/UtilsFrTest.php new file mode 100644 index 00000000..890308d3 --- /dev/null +++ b/tests/languages/fr/UtilsFrTest.php | |||
@@ -0,0 +1,25 @@ | |||
1 | <?php | ||
2 | |||
3 | require_once 'tests/UtilsTest.php'; | ||
4 | |||
5 | |||
6 | class UtilsFrTest extends UtilsTest | ||
7 | { | ||
8 | /** | ||
9 | * Test date_format(). | ||
10 | */ | ||
11 | public function testDateFormat() | ||
12 | { | ||
13 | $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); | ||
14 | $this->assertRegExp('/1 janvier 2017 (à )?10:11:12 UTC\+0?3(:00)?/', format_date($date)); | ||
15 | } | ||
16 | |||
17 | /** | ||
18 | * Test date_format() using builtin PHP function strftime. | ||
19 | */ | ||
20 | public function testDateFormatDefault() | ||
21 | { | ||
22 | $date = DateTime::createFromFormat('Ymd_His', '20170101_101112'); | ||
23 | $this->assertEquals('dim. 01 janv. 2017 10:11:12 EAT', format_date($date, false)); | ||
24 | } | ||
25 | } | ||
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html index a9712704..9bc3ba1a 100644 --- a/tpl/default/linklist.html +++ b/tpl/default/linklist.html | |||
@@ -171,10 +171,11 @@ | |||
171 | <div class="linklist-item-infos-dateblock pure-u-lg-3-8 pure-u-1"> | 171 | <div class="linklist-item-infos-dateblock pure-u-lg-3-8 pure-u-1"> |
172 | <a href="?{$value.shorturl}" title="{'Permalink'|t}"> | 172 | <a href="?{$value.shorturl}" title="{'Permalink'|t}"> |
173 | {if="!$hide_timestamps || isLoggedIn()"} | 173 | {if="!$hide_timestamps || isLoggedIn()"} |
174 | {$updated=$value.updated_timestamp ? 'Edited: '. strftime('%c', $value.updated_timestamp) : 'Permalink'} | 174 | {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'} |
175 | <span class="linkdate" title="{$updated}"> | 175 | <span class="linkdate" title="{$updated}"> |
176 | <i class="fa fa-clock-o"></i> | 176 | <i class="fa fa-clock-o"></i> |
177 | {function="strftime('%c', $value.timestamp)"}{if="$value.updated_timestamp"}*{/if} | 177 | {$value.created|format_date} |
178 | {if="$value.updated_timestamp"}*{/if} | ||
178 | · | 179 | · |
179 | </span> | 180 | </span> |
180 | {/if} | 181 | {/if} |
diff --git a/tpl/vintage/linklist.html b/tpl/vintage/linklist.html index 5accc92f..fc116667 100644 --- a/tpl/vintage/linklist.html +++ b/tpl/vintage/linklist.html | |||
@@ -99,11 +99,11 @@ | |||
99 | <br> | 99 | <br> |
100 | {if="$value.description"}<div class="linkdescription">{$value.description}</div>{/if} | 100 | {if="$value.description"}<div class="linkdescription">{$value.description}</div>{/if} |
101 | {if="!$hide_timestamps || isLoggedIn()"} | 101 | {if="!$hide_timestamps || isLoggedIn()"} |
102 | {$updated=$value.updated_timestamp ? 'Edited: '. strftime('%c', $value.updated_timestamp) : 'Permalink'} | 102 | {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'} |
103 | <span class="linkdate" title="Permalink"> | 103 | <span class="linkdate" title="Permalink"> |
104 | <a href="?{$value.shorturl}"> | 104 | <a href="?{$value.shorturl}"> |
105 | <span title="{$updated}"> | 105 | <span title="{$updated}"> |
106 | {function="strftime('%c', $value.timestamp)"} | 106 | {$value.created|format_date} |
107 | {if="$value.updated_timestamp"}*{/if} | 107 | {if="$value.updated_timestamp"}*{/if} |
108 | </span> | 108 | </span> |
109 | - permalink | 109 | - permalink |