From 1255a42cfed9ce419962c6cf29181a66c7e22bb8 Mon Sep 17 00:00:00 2001
From: ArthurHoaro <arthur@hoa.ro>
Date: Sat, 7 Jan 2017 14:28:58 +0100
Subject: Improve autoLocale() detection

  - Creates arrays_combination function to cover all cases
  - add the underscore separator in the regex
  - add `utf8` encoding in addition to `UTF-8`
---
 application/Utils.php | 51 ++++++++++++++++++++++++++++++++++++++++++---------
 tests/UtilsTest.php   | 20 ++++++++++++++++++++
 2 files changed, 62 insertions(+), 9 deletions(-)

diff --git a/application/Utils.php b/application/Utils.php
index 35d65224..19fb7116 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -216,22 +216,55 @@ function is_session_id_valid($sessionId)
 function autoLocale($headerLocale)
 {
     // Default if browser does not send HTTP_ACCEPT_LANGUAGE
-    $attempts = array('en_US');
+    $attempts = array('en_US', 'en_US.utf8', 'en_US.UTF-8');
     if (isset($headerLocale)) {
         // (It's a bit crude, but it works very well. Preferred language is always presented first.)
-        if (preg_match('/([a-z]{2})-?([a-z]{2})?/i', $headerLocale, $matches)) {
-            $loc = $matches[1] . (!empty($matches[2]) ? '_' . strtoupper($matches[2]) : '');
-            $attempts = array(
-                $loc.'.UTF-8', $loc, str_replace('_', '-', $loc).'.UTF-8', str_replace('_', '-', $loc),
-                $loc . '_' . strtoupper($loc).'.UTF-8', $loc . '_' . strtoupper($loc),
-                $loc . '_' . $loc.'.UTF-8', $loc . '_' . $loc, $loc . '-' . strtoupper($loc).'.UTF-8',
-                $loc . '-' . strtoupper($loc), $loc . '-' . $loc.'.UTF-8', $loc . '-' . $loc
-            );
+        if (preg_match('/([a-z]{2,3})[-_]?([a-z]{2})?/i', $headerLocale, $matches)) {
+            $first = [strtolower($matches[1]), strtoupper($matches[1])];
+            $separators = ['_', '-'];
+            $encodings = ['utf8', 'UTF-8'];
+            if (!empty($matches[2])) {
+                $second = [strtoupper($matches[2]), strtolower($matches[2])];
+                $attempts = arrays_combination([$first, $separators, $second, ['.'], $encodings]);
+            } else {
+                $attempts = arrays_combination([$first, $separators, $first, ['.'], $encodings]);
+            }
         }
     }
     setlocale(LC_ALL, $attempts);
 }
 
+/**
+ * Combine multiple arrays of string to get all possible strings.
+ * The order is important because this doesn't shuffle the entries.
+ *
+ * Example:
+ *   [['a'], ['b', 'c']]
+ * will generate:
+ *   - ab
+ *   - ac
+ *
+ * TODO PHP 5.6: use the `...` operator instead of an array of array.
+ *
+ * @param array $items array of array of string
+ *
+ * @return array Combined string from the input array.
+ */
+function arrays_combination($items)
+{
+    $out = [''];
+    foreach ($items as $item) {
+        $add = [];
+        foreach ($item as $element) {
+            foreach ($out as $key => $existingEntry) {
+                $add[] = $existingEntry . $element;
+            }
+        }
+        $out = $add;
+    }
+    return $out;
+}
+
 /**
  * Generates a default API secret.
  *
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php
index c885f552..b8f608b9 100644
--- a/tests/UtilsTest.php
+++ b/tests/UtilsTest.php
@@ -282,4 +282,24 @@ class UtilsTest extends PHPUnit_Framework_TestCase
         $this->assertEquals('', normalize_spaces(''));
         $this->assertEquals(null, normalize_spaces(null));
     }
+
+    /**
+     * Test arrays_combine
+     */
+    public function testArraysCombination()
+    {
+        $arr = [['ab', 'cd'], ['ef', 'gh'], ['ij', 'kl'], ['m']];
+        $expected = [
+            'abefijm',
+            'cdefijm',
+            'abghijm',
+            'cdghijm',
+            'abefklm',
+            'cdefklm',
+            'abghklm',
+            'cdghklm',
+        ];
+        $this->assertEquals($expected, arrays_combination($arr));
+    }
+
 }
-- 
cgit v1.2.3


From 52b503105d389d1796698114573ff618b2ad34a2 Mon Sep 17 00:00:00 2001
From: ArthurHoaro <arthur@hoa.ro>
Date: Sat, 7 Jan 2017 14:30:42 +0100
Subject: Improve datetime display

Use php-intl extension to display datetimes a bit more nicely, depending on the locale.

What changes:

  * the day is no longer displayed
  * day number and month are ordered according to the locale
  * the timezone is more readable (UTC+1 instead of CET)
---
 application/Utils.php              | 72 +++++++++++++++++++++++++++-----------
 tests/UtilsTest.php                | 46 ++++++++++++++++++------
 tests/languages/bootstrap.php      |  7 ++++
 tests/languages/de/UtilsDeTest.php | 25 +++++++++++++
 tests/languages/en/UtilsEnTest.php | 25 +++++++++++++
 tests/languages/fr/UtilsFrTest.php | 25 +++++++++++++
 tpl/default/linklist.html          |  5 +--
 tpl/vintage/linklist.html          |  4 +--
 8 files changed, 174 insertions(+), 35 deletions(-)
 create mode 100644 tests/languages/bootstrap.php
 create mode 100644 tests/languages/de/UtilsDeTest.php
 create mode 100644 tests/languages/en/UtilsEnTest.php
 create mode 100644 tests/languages/fr/UtilsFrTest.php

diff --git a/application/Utils.php b/application/Utils.php
index 19fb7116..a936b09f 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -225,44 +225,46 @@ function autoLocale($headerLocale)
             $encodings = ['utf8', 'UTF-8'];
             if (!empty($matches[2])) {
                 $second = [strtoupper($matches[2]), strtolower($matches[2])];
-                $attempts = arrays_combination([$first, $separators, $second, ['.'], $encodings]);
+                $attempts = cartesian_product_generator([$first, $separators, $second, ['.'], $encodings]);
             } else {
-                $attempts = arrays_combination([$first, $separators, $first, ['.'], $encodings]);
+                $attempts = cartesian_product_generator([$first, $separators, $first, ['.'], $encodings]);
             }
         }
     }
-    setlocale(LC_ALL, $attempts);
+    setlocale(LC_ALL, implode('implode', iterator_to_array($attempts)));
 }
 
 /**
- * Combine multiple arrays of string to get all possible strings.
- * The order is important because this doesn't shuffle the entries.
+ * Build a Generator object representing the cartesian product from given $items.
  *
  * Example:
  *   [['a'], ['b', 'c']]
  * will generate:
- *   - ab
- *   - ac
- *
- * TODO PHP 5.6: use the `...` operator instead of an array of array.
+ *   [
+ *      ['a', 'b'],
+ *      ['a', 'c'],
+ *   ]
  *
  * @param array $items array of array of string
  *
- * @return array Combined string from the input array.
+ * @return Generator representing the cartesian product of given array.
+ *
+ * @see https://en.wikipedia.org/wiki/Cartesian_product
  */
-function arrays_combination($items)
+function cartesian_product_generator($items)
 {
-    $out = [''];
-    foreach ($items as $item) {
-        $add = [];
-        foreach ($item as $element) {
-            foreach ($out as $key => $existingEntry) {
-                $add[] = $existingEntry . $element;
-            }
+    if (empty($items)) {
+        yield [];
+    }
+    $subArray = array_pop($items);
+    if (empty($subArray)) {
+        return;
+    }
+    foreach (cartesian_product_generator($items) as $item) {
+        foreach ($subArray as $value) {
+            yield $item + [count($item) => $value];
         }
-        $out = $add;
     }
-    return $out;
 }
 
 /**
@@ -303,3 +305,33 @@ function normalize_spaces($string)
 {
     return preg_replace('/\s{2,}/', ' ', trim($string));
 }
+
+/**
+ * Format the date according to the locale.
+ *
+ * Requires php-intl to display international datetimes,
+ * otherwise default format '%c' will be returned.
+ *
+ * @param DateTime $date to format.
+ * @param bool     $intl Use international format if true.
+ *
+ * @return bool|string Formatted date, or false if the input is invalid.
+ */
+function format_date($date, $intl = true)
+{
+    if (! $date instanceof DateTime) {
+        return false;
+    }
+
+    if (! $intl || ! class_exists('IntlDateFormatter')) {
+        return strftime('%c', $date->getTimestamp());
+    }
+
+    $formatter = new IntlDateFormatter(
+        setlocale(LC_TIME, 0),
+        IntlDateFormatter::LONG,
+        IntlDateFormatter::LONG
+    );
+
+    return $formatter->format($date);
+}
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php
index b8f608b9..e70cc1ae 100644
--- a/tests/UtilsTest.php
+++ b/tests/UtilsTest.php
@@ -23,7 +23,12 @@ class UtilsTest extends PHPUnit_Framework_TestCase
 
     // Expected log date format
     protected static $dateFormat = 'Y/m/d H:i:s';
-    
+
+    /**
+     * @var string Save the current timezone.
+     */
+    protected static $defaultTimeZone;
+
 
     /**
      * Assign reference data
@@ -31,6 +36,17 @@ class UtilsTest extends PHPUnit_Framework_TestCase
     public static function setUpBeforeClass()
     {
         self::$sidHashes = ReferenceSessionIdHashes::getHashes();
+        self::$defaultTimeZone = date_default_timezone_get();
+        // Timezone without DST for test consistency
+        date_default_timezone_set('Africa/Nairobi');
+    }
+
+    /**
+     * Reset the timezone
+     */
+    public static function tearDownAfterClass()
+    {
+        date_default_timezone_set(self::$defaultTimeZone);
     }
 
     /**
@@ -286,20 +302,28 @@ class UtilsTest extends PHPUnit_Framework_TestCase
     /**
      * Test arrays_combine
      */
-    public function testArraysCombination()
+    public function testCartesianProductGenerator()
     {
         $arr = [['ab', 'cd'], ['ef', 'gh'], ['ij', 'kl'], ['m']];
         $expected = [
-            'abefijm',
-            'cdefijm',
-            'abghijm',
-            'cdghijm',
-            'abefklm',
-            'cdefklm',
-            'abghklm',
-            'cdghklm',
+            ['ab', 'ef', 'ij', 'm'],
+            ['ab', 'ef', 'kl', 'm'],
+            ['ab', 'gh', 'ij', 'm'],
+            ['ab', 'gh', 'kl', 'm'],
+            ['cd', 'ef', 'ij', 'm'],
+            ['cd', 'ef', 'kl', 'm'],
+            ['cd', 'gh', 'ij', 'm'],
+            ['cd', 'gh', 'kl', 'm'],
         ];
-        $this->assertEquals($expected, arrays_combination($arr));
+        $this->assertEquals($expected, iterator_to_array(cartesian_product_generator($arr)));
     }
 
+    /**
+     * Test date_format() with invalid parameter.
+     */
+    public function testDateFormatInvalid()
+    {
+        $this->assertFalse(format_date([]));
+        $this->assertFalse(format_date(null));
+    }
 }
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 @@
+<?php
+if (! empty('UT_LOCALE')) {
+    setlocale(LC_ALL, getenv('UT_LOCALE'));
+}
+
+require_once 'vendor/autoload.php';
+
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 @@
+<?php
+
+require_once 'tests/UtilsTest.php';
+
+
+class UtilsDeTest extends UtilsTest
+{
+    /**
+     * Test date_format().
+     */
+    public function testDateFormat()
+    {
+        $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
+        $this->assertRegExp('/1. Januar 2017 (um )?10:11:12 GMT\+0?3(:00)?/', format_date($date, true));
+    }
+
+    /**
+     * Test date_format() using builtin PHP function strftime.
+     */
+    public function testDateFormatDefault()
+    {
+        $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
+        $this->assertEquals('So 01 Jan 2017 10:11:12 EAT', format_date($date, false));
+    }
+}
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 @@
+<?php
+
+require_once 'tests/UtilsTest.php';
+
+
+class UtilsEnTest extends UtilsTest
+{
+    /**
+     * Test date_format().
+     */
+    public function testDateFormat()
+    {
+        $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
+        $this->assertRegExp('/January 1, 2017 (at )?10:11:12 AM GMT\+0?3(:00)?/', format_date($date, true));
+    }
+
+    /**
+     * Test date_format() using builtin PHP function strftime.
+     */
+    public function testDateFormatDefault()
+    {
+        $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
+        $this->assertEquals('Sun 01 Jan 2017 10:11:12 AM EAT', format_date($date, false));
+    }
+}
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 @@
+<?php
+
+require_once 'tests/UtilsTest.php';
+
+
+class UtilsFrTest extends UtilsTest
+{
+    /**
+     * Test date_format().
+     */
+    public function testDateFormat()
+    {
+        $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
+        $this->assertRegExp('/1 janvier 2017 (à )?10:11:12 UTC\+0?3(:00)?/', format_date($date));
+    }
+
+    /**
+     * Test date_format() using builtin PHP function strftime.
+     */
+    public function testDateFormatDefault()
+    {
+        $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
+        $this->assertEquals('dim. 01 janv. 2017 10:11:12 EAT', format_date($date, false));
+    }
+}
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 @@
               <div class="linklist-item-infos-dateblock pure-u-lg-3-8 pure-u-1">
                 <a href="?{$value.shorturl}" title="{'Permalink'|t}">
                   {if="!$hide_timestamps || isLoggedIn()"}
-                    {$updated=$value.updated_timestamp ? 'Edited: '. strftime('%c', $value.updated_timestamp) : 'Permalink'}
+                    {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'}
                     <span class="linkdate" title="{$updated}">
                       <i class="fa fa-clock-o"></i>
-                      {function="strftime('%c', $value.timestamp)"}{if="$value.updated_timestamp"}*{/if}
+                      {$value.created|format_date}
+                      {if="$value.updated_timestamp"}*{/if}
                       &middot;
                     </span>
                   {/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 @@
                 <br>
                 {if="$value.description"}<div class="linkdescription">{$value.description}</div>{/if}
                 {if="!$hide_timestamps || isLoggedIn()"}
-                    {$updated=$value.updated_timestamp ? 'Edited: '. strftime('%c', $value.updated_timestamp) : 'Permalink'}
+                    {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'}
                     <span class="linkdate" title="Permalink">
                         <a href="?{$value.shorturl}">
                             <span title="{$updated}">
-                                {function="strftime('%c', $value.timestamp)"}
+                                {$value.created|format_date}
                                 {if="$value.updated_timestamp"}*{/if}
                             </span>
                             - permalink
-- 
cgit v1.2.3


From 6c7d68645409cfad3858b5f252f5a49b148e3b53 Mon Sep 17 00:00:00 2001
From: ArthurHoaro <arthur@hoa.ro>
Date: Sun, 15 Jan 2017 16:31:53 +0100
Subject: Run languages tests using PHPUnit test suites

---
 .travis.yml   |  6 ++++++
 Makefile      | 16 ++++++++++++++--
 composer.json |  3 ++-
 phpunit.xml   | 20 ++++++++++++++++----
 4 files changed, 38 insertions(+), 7 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 03071a47..2a5ff5e3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,11 @@
 sudo: false
 language: php
+addons:
+  apt:
+    packages:
+      - locales
+      - language-pack-de
+      - language-pack-fr
 cache:
   directories:
     - $HOME/.composer/cache
diff --git a/Makefile b/Makefile
index f3065b77..1d8a73a2 100644
--- a/Makefile
+++ b/Makefile
@@ -124,8 +124,20 @@ test:
 	@echo "-------"
 	@echo "PHPUNIT"
 	@echo "-------"
-	@mkdir -p sandbox
-	@$(BIN)/phpunit tests
+	@mkdir -p sandbox coverage
+	@$(BIN)/phpunit --coverage-php coverage/main.cov --testsuite unit-tests
+
+locale_test_%:
+	@UT_LOCALE=$*.utf8 \
+		$(BIN)/phpunit \
+		--coverage-php coverage/$(firstword $(subst _, ,$*)).cov \
+		--bootstrap tests/languages/bootstrap.php \
+		--testsuite language-$(firstword $(subst _, ,$*))
+
+all_tests: test locale_test_de_DE locale_test_en_US locale_test_fr_FR
+	@$(BIN)/phpcov merge --html coverage coverage
+	@# --text doesn't work with phpunit 4.* (v5 requires PHP 5.6)
+	@#$(BIN)/phpcov merge --text coverage/txt coverage
 
 ##
 # Custom release archive generation
diff --git a/composer.json b/composer.json
index b82aceef..70b87bb9 100644
--- a/composer.json
+++ b/composer.json
@@ -20,7 +20,8 @@
         "phpmd/phpmd" : "@stable",
         "phpunit/phpunit": "4.8.*",
         "sebastian/phpcpd": "*",
-        "squizlabs/php_codesniffer": "2.*"
+        "squizlabs/php_codesniffer": "2.*",
+        "phpunit/phpcov": "*"
     },
     "autoload": {
         "psr-4": {
diff --git a/phpunit.xml b/phpunit.xml
index d6e01c35..8b66e6c5 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -3,13 +3,25 @@
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.5/phpunit.xsd"
     colors="true">
+  <testsuites>
+    <testsuite name="unit-tests">
+      <directory>tests</directory>
+      <exclude>tests/languages</exclude>
+    </testsuite>
+    <testsuite name="language-de">
+      <directory>tests/languages/de</directory>
+    </testsuite>
+    <testsuite name="language-en">
+      <directory>tests/languages/en</directory>
+    </testsuite>
+    <testsuite name="language-fr">
+      <directory>tests/languages/fr</directory>
+    </testsuite>
+  </testsuites>
+
   <filter>
     <whitelist addUncoveredFilesFromWhitelist="true">
       <directory suffix=".php">application</directory>
     </whitelist>
   </filter>
-  <logging>
-    <log type="coverage-html" target="coverage" lowUpperBound="30" highLowerBound="80"/>
-    <log type="coverage-text" target="php://stdout" showUncoveredFiles="true"/>
-  </logging>
 </phpunit>
-- 
cgit v1.2.3


From 36c8fb1ef869c29e783f0dd5ebef2fb5566e2611 Mon Sep 17 00:00:00 2001
From: ArthurHoaro <arthur@hoa.ro>
Date: Mon, 6 Mar 2017 20:34:02 +0100
Subject: Use all_tests target in Travis CI

---
 .travis.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.travis.yml b/.travis.yml
index 2a5ff5e3..59b86c08 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -20,4 +20,4 @@ install:
 script:
   - make clean
   - make check_permissions
-  - make test
+  - make all_tests
-- 
cgit v1.2.3