From 9ec0a61156192484ca90a8dc88b7c23b26129755 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 2 Sep 2017 15:10:44 +0200 Subject: [PATCH 01/36] Performances: reorder links when they're written instead of read relates to #891 --- application/LinkDB.php | 17 ++++++++--------- application/Updater.php | 8 ++++++++ tests/LinkFilterTest.php | 13 ++++++++++++- tests/utils/ReferenceLinkDB.php | 23 +++++++++++++++++++++++ 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/application/LinkDB.php b/application/LinkDB.php index 22c1f0ab..eace625e 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php @@ -289,13 +289,15 @@ private function read() return; } + $this->urls = []; + $this->ids = []; $this->links = FileUtils::readFlatDB($this->datastore, []); $toremove = array(); foreach ($this->links as $key => &$link) { if (! $this->loggedIn && $link['private'] != 0) { // Transition for not upgraded databases. - $toremove[] = $key; + unset($this->links[$key]); continue; } @@ -329,14 +331,10 @@ private function read() } $link['shorturl'] = smallHash($link['linkdate']); } - } - // If user is not logged in, filter private links. - foreach ($toremove as $offset) { - unset($this->links[$offset]); + $this->urls[$link['url']] = $key; + $this->ids[$link['id']] = $key; } - - $this->reorder(); } /** @@ -346,6 +344,7 @@ private function read() */ private function write() { + $this->reorder(); FileUtils::writeFlatDB($this->datastore, $this->links); } @@ -528,8 +527,8 @@ public function reorder($order = 'DESC') return $a['created'] < $b['created'] ? 1 * $order : -1 * $order; }); - $this->urls = array(); - $this->ids = array(); + $this->urls = []; + $this->ids = []; foreach ($this->links as $key => $link) { $this->urls[$link['url']] = $key; $this->ids[$link['id']] = $key; diff --git a/application/Updater.php b/application/Updater.php index 40a15906..0702158a 100644 --- a/application/Updater.php +++ b/application/Updater.php @@ -436,6 +436,14 @@ public function updateMethodResetHistoryFile() } return true; } + + /** + * Save the datastore -> the link order is now applied when links are saved. + */ + public function updateMethodReorderDatastore() + { + $this->linkDB->save($this->conf->get('resource.page_cache')); + } } /** diff --git a/tests/LinkFilterTest.php b/tests/LinkFilterTest.php index d796d3a3..9cd6dbd4 100644 --- a/tests/LinkFilterTest.php +++ b/tests/LinkFilterTest.php @@ -7,6 +7,10 @@ */ class LinkFilterTest extends PHPUnit_Framework_TestCase { + /** + * @var string Test datastore path. + */ + protected static $testDatastore = 'sandbox/datastore.php'; /** * @var LinkFilter instance. */ @@ -17,13 +21,20 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase */ protected static $refDB; + /** + * @var LinkDB instance + */ + protected static $linkDB; + /** * Instanciate linkFilter with ReferenceLinkDB data. */ public static function setUpBeforeClass() { self::$refDB = new ReferenceLinkDB(); - self::$linkFilter = new LinkFilter(self::$refDB->getLinks()); + self::$refDB->write(self::$testDatastore); + self::$linkDB = new LinkDB(self::$testDatastore, true, false); + self::$linkFilter = new LinkFilter(self::$linkDB); } /** diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php index f09eebc1..e887aa78 100644 --- a/tests/utils/ReferenceLinkDB.php +++ b/tests/utils/ReferenceLinkDB.php @@ -141,12 +141,34 @@ protected function addLink($id, $title, $url, $description, $private, $date, $ta */ public function write($filename) { + $this->reorder(); file_put_contents( $filename, '_links))).' */ ?>' ); } + /** + * Reorder links by creation date (newest first). + * + * Also update the urls and ids mapping arrays. + * + * @param string $order ASC|DESC + */ + public function reorder($order = 'DESC') + { + // backward compatibility: ignore reorder if the the `created` field doesn't exist + if (! isset(array_values($this->_links)[0]['created'])) { + return; + } + + $order = $order === 'ASC' ? -1 : 1; + // Reorder array by dates. + usort($this->_links, function($a, $b) use ($order) { + return $a['created'] < $b['created'] ? 1 * $order : -1 * $order; + }); + } + /** * Returns the number of links in the reference data */ @@ -187,6 +209,7 @@ public function countUntaggedLinks() public function getLinks() { + $this->reorder(); return $this->_links; } From 1a216faecb5c114afbf36ecbac8ec3f795309eba Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Sun, 9 Apr 2017 14:50:13 +0200 Subject: [PATCH 02/36] docker: switch to Alpine Linux for the master image Relates to https://github.com/shaarli/Shaarli/issues/843 Changed: - switch base image from Debian:Jessie to Alpine:3.6 - switch to PHP 7.1 - switch from supervisord to s6 to manage services See: - https://alpinelinux.org/ - https://wiki.alpinelinux.org/wiki/Nginx_with_PHP - http://www.skarnet.org/software/s6/ - http://www.skarnet.org/software/s6/s6-svscan.html - http://www.skarnet.org/software/s6/s6-svc.html - http://www.skarnet.org/software/s6/s6-svstat.html Signed-off-by: VirtualTam --- Makefile | 2 +- docker/alpine/Dockerfile.master | 47 +++++++++++++++++++ docker/{production => alpine}/IMAGE.md | 0 .../{production/stable => alpine}/nginx.conf | 5 +- docker/alpine/php-fpm.conf | 16 +++++++ docker/alpine/services.d/.s6-svscan/finish | 2 + docker/alpine/services.d/nginx/run | 2 + docker/alpine/services.d/php-fpm/run | 2 + docker/production/Dockerfile | 37 --------------- docker/production/supervised.conf | 13 ----- docker/{production => }/stable/Dockerfile | 0 docker/{production => }/stable/IMAGE.md | 0 docker/{production => stable}/nginx.conf | 0 .../{production => }/stable/supervised.conf | 0 14 files changed, 73 insertions(+), 53 deletions(-) create mode 100644 docker/alpine/Dockerfile.master rename docker/{production => alpine}/IMAGE.md (100%) rename docker/{production/stable => alpine}/nginx.conf (94%) create mode 100644 docker/alpine/php-fpm.conf create mode 100755 docker/alpine/services.d/.s6-svscan/finish create mode 100755 docker/alpine/services.d/nginx/run create mode 100755 docker/alpine/services.d/php-fpm/run delete mode 100644 docker/production/Dockerfile delete mode 100644 docker/production/supervised.conf rename docker/{production => }/stable/Dockerfile (100%) rename docker/{production => }/stable/IMAGE.md (100%) rename docker/{production => stable}/nginx.conf (100%) rename docker/{production => }/stable/supervised.conf (100%) diff --git a/Makefile b/Makefile index a3696ec9..656c27b0 100644 --- a/Makefile +++ b/Makefile @@ -115,7 +115,7 @@ check_permissions: @echo "----------------------" @echo "Check file permissions" @echo "----------------------" - @for file in `git ls-files`; do \ + @for file in `git ls-files | grep -v docker`; do \ if [ -x $$file ]; then \ errors=true; \ echo "$${file} is executable"; \ diff --git a/docker/alpine/Dockerfile.master b/docker/alpine/Dockerfile.master new file mode 100644 index 00000000..58f7c6e7 --- /dev/null +++ b/docker/alpine/Dockerfile.master @@ -0,0 +1,47 @@ +FROM alpine:3.6 +MAINTAINER Shaarli Community + +RUN apk --update --no-cache add \ + ca-certificates \ + curl \ + nginx \ + php7 \ + php7-ctype \ + php7-curl \ + php7-fpm \ + php7-gd \ + php7-iconv \ + php7-intl \ + php7-json \ + php7-mbstring \ + php7-openssl \ + php7-phar \ + php7-session \ + php7-xml \ + php7-zlib \ + s6 + +COPY nginx.conf /etc/nginx/nginx.conf +COPY php-fpm.conf /etc/php7/php-fpm.conf +COPY services.d /etc/services.d + +RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \ + && rm -rf /etc/php7/php-fpm.d/www.conf \ + && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \ + && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini + + +WORKDIR /var/www +RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \ + && mv Shaarli-master shaarli \ + && cd shaarli \ + && composer --prefer-dist --no-dev install \ + && rm -rf ~/.composer \ + && chown -R nginx:nginx . + +VOLUME /var/www/shaarli/data + +EXPOSE 80 + +ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"] +CMD [] diff --git a/docker/production/IMAGE.md b/docker/alpine/IMAGE.md similarity index 100% rename from docker/production/IMAGE.md rename to docker/alpine/IMAGE.md diff --git a/docker/production/stable/nginx.conf b/docker/alpine/nginx.conf similarity index 94% rename from docker/production/stable/nginx.conf rename to docker/alpine/nginx.conf index e8754d9b..07fba33f 100644 --- a/docker/production/stable/nginx.conf +++ b/docker/alpine/nginx.conf @@ -1,6 +1,7 @@ -user www-data www-data; +user nginx nginx; daemon off; worker_processes 4; +pid /var/run/nginx.pid; events { worker_connections 768; @@ -59,7 +60,7 @@ http { fastcgi_split_path_info ^(.+\.php)(/.+)$; # filter and proxy PHP requests to PHP-FPM - fastcgi_pass unix:/var/run/php5-fpm.sock; + fastcgi_pass unix:/var/run/php-fpm.sock; fastcgi_index index.php; include fastcgi.conf; } diff --git a/docker/alpine/php-fpm.conf b/docker/alpine/php-fpm.conf new file mode 100644 index 00000000..0843c164 --- /dev/null +++ b/docker/alpine/php-fpm.conf @@ -0,0 +1,16 @@ +[global] +daemonize = no + +[www] +user = nginx +group = nginx +listen.owner = nginx +listen.group = nginx +catch_workers_output = yes +listen = /var/run/php-fpm.sock +pm = dynamic +pm.max_children = 20 +pm.start_servers = 1 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +pm.max_requests = 2048 diff --git a/docker/alpine/services.d/.s6-svscan/finish b/docker/alpine/services.d/.s6-svscan/finish new file mode 100755 index 00000000..1dadeeaf --- /dev/null +++ b/docker/alpine/services.d/.s6-svscan/finish @@ -0,0 +1,2 @@ +#!/bin/sh +/bin/true diff --git a/docker/alpine/services.d/nginx/run b/docker/alpine/services.d/nginx/run new file mode 100755 index 00000000..21e7b0d6 --- /dev/null +++ b/docker/alpine/services.d/nginx/run @@ -0,0 +1,2 @@ +#!/bin/execlineb -P +nginx diff --git a/docker/alpine/services.d/php-fpm/run b/docker/alpine/services.d/php-fpm/run new file mode 100755 index 00000000..21dd0107 --- /dev/null +++ b/docker/alpine/services.d/php-fpm/run @@ -0,0 +1,2 @@ +#!/bin/execlineb -P +php-fpm7 -F diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile deleted file mode 100644 index d0509115..00000000 --- a/docker/production/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -FROM debian:jessie -MAINTAINER Shaarli Community - -ENV TERM dumb -RUN apt-get update \ - && apt-get install --no-install-recommends -y \ - ca-certificates \ - curl \ - nginx-light \ - php5-curl \ - php5-fpm \ - php5-gd \ - php5-intl \ - supervisor \ - && apt-get clean - -RUN sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php5/fpm/php.ini -RUN sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php5/fpm/php.ini -COPY nginx.conf /etc/nginx/nginx.conf -COPY supervised.conf /etc/supervisor/conf.d/supervised.conf - -ADD https://getcomposer.org/composer.phar /usr/local/bin/composer -RUN chmod 755 /usr/local/bin/composer - -WORKDIR /var/www -RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \ - && mv Shaarli-master shaarli \ - && cd shaarli \ - && composer --prefer-dist --no-dev install -RUN rm -rf html \ - && chown -R www-data:www-data . - -VOLUME /var/www/shaarli/data - -EXPOSE 80 - -CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"] diff --git a/docker/production/supervised.conf b/docker/production/supervised.conf deleted file mode 100644 index 5acd9795..00000000 --- a/docker/production/supervised.conf +++ /dev/null @@ -1,13 +0,0 @@ -[program:php5-fpm] -command=/usr/sbin/php5-fpm -F -priority=5 -autostart=true -autorestart=true - -[program:nginx] -command=/usr/sbin/nginx -priority=10 -autostart=true -autorestart=true -stdout_events_enabled=true -stderr_events_enabled=true diff --git a/docker/production/stable/Dockerfile b/docker/stable/Dockerfile similarity index 100% rename from docker/production/stable/Dockerfile rename to docker/stable/Dockerfile diff --git a/docker/production/stable/IMAGE.md b/docker/stable/IMAGE.md similarity index 100% rename from docker/production/stable/IMAGE.md rename to docker/stable/IMAGE.md diff --git a/docker/production/nginx.conf b/docker/stable/nginx.conf similarity index 100% rename from docker/production/nginx.conf rename to docker/stable/nginx.conf diff --git a/docker/production/stable/supervised.conf b/docker/stable/supervised.conf similarity index 100% rename from docker/production/stable/supervised.conf rename to docker/stable/supervised.conf From e3a3cc0da85925d08df29a2146b54b4159d5a14b Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Tue, 3 Oct 2017 20:07:46 +0200 Subject: [PATCH 03/36] docker: rename resources for the stable image Signed-off-by: VirtualTam --- docker/{stable/Dockerfile => debian/Dockerfile.stable} | 0 docker/{stable => debian}/IMAGE.md | 0 docker/{stable => debian}/nginx.conf | 0 docker/{stable => debian}/supervised.conf | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename docker/{stable/Dockerfile => debian/Dockerfile.stable} (100%) rename docker/{stable => debian}/IMAGE.md (100%) rename docker/{stable => debian}/nginx.conf (100%) rename docker/{stable => debian}/supervised.conf (100%) diff --git a/docker/stable/Dockerfile b/docker/debian/Dockerfile.stable similarity index 100% rename from docker/stable/Dockerfile rename to docker/debian/Dockerfile.stable diff --git a/docker/stable/IMAGE.md b/docker/debian/IMAGE.md similarity index 100% rename from docker/stable/IMAGE.md rename to docker/debian/IMAGE.md diff --git a/docker/stable/nginx.conf b/docker/debian/nginx.conf similarity index 100% rename from docker/stable/nginx.conf rename to docker/debian/nginx.conf diff --git a/docker/stable/supervised.conf b/docker/debian/supervised.conf similarity index 100% rename from docker/stable/supervised.conf rename to docker/debian/supervised.conf From 78865393a687a4c057109fa51f22934ad078d482 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 7 Oct 2017 12:27:50 +0200 Subject: [PATCH 04/36] Badge version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 100ff46b..c1050027 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ _It is designed to be personal (single-user), fast and handy._ [![](https://img.shields.io/badge/stable-v0.8.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4) [![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli) • -[![](https://img.shields.io/badge/latest-v0.9.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) +[![](https://img.shields.io/badge/latest-v0.9.2-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.2) [![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli) • [![](https://img.shields.io/badge/master-v0.9.x-blue.svg)](https://github.com/shaarli/Shaarli) From ba6245670d071abcc6e90c0f277222ec5e55b413 Mon Sep 17 00:00:00 2001 From: Daniel Jakots Date: Sat, 7 Oct 2017 09:35:40 -0400 Subject: [PATCH 05/36] Fix link in Upgrade-and-migration.md --- doc/md/Upgrade-and-migration.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/md/Upgrade-and-migration.md b/doc/md/Upgrade-and-migration.md index b3a08764..7033cd41 100644 --- a/doc/md/Upgrade-and-migration.md +++ b/doc/md/Upgrade-and-migration.md @@ -14,7 +14,7 @@ Shaarli stores all user data under the `data` directory: - `data/ipbans.php` - banned IP addresses - `data/updates.txt` - contains all automatic update to the configuration and datastore files already run -See [Shaarli configuration](Shaarli configuration) for more information about Shaarli resources. +See [Shaarli configuration](Shaarli-configuration) for more information about Shaarli resources. It is recommended to backup this repository _before_ starting updating/upgrading Shaarli: @@ -27,7 +27,7 @@ As all user data is kept under `data`, this is the only directory you need to wo - backup the `data` directory - install or update Shaarli: - - fresh installation - see [Download and installation](Download and installation) + - fresh installation - see [Download and installation](Download-and-installation) - update - see the following sections - check or restore the `data` directory @@ -35,11 +35,11 @@ As all user data is kept under `data`, this is the only directory you need to wo All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page. -We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download and installation) for `git` complete instructions. +We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download-and-installation) for `git` complete instructions. Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory! -After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli configuration) for more details). +After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli-configuration) for more details). ## Upgrading with Git @@ -173,7 +173,7 @@ Total 3317 (delta 2050), reused 3301 (delta 2034)to #### Step 3: configuration -After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli configuration) for more details). +After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli-configuration) for more details). ## Troubleshooting From 66e74d50d38a6fea8fc904a1746157633de7cc65 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 7 Oct 2017 16:40:16 +0200 Subject: [PATCH 06/36] Don't write History for link import With large imports it has a large impact on performances and isn't really useful. Instead, write an IMPORT event, which let client using the history service resync its DB. -> 15k link import done in 6 seconds. Fixes #985 --- application/History.php | 16 ++++ application/NetscapeBookmarkUtils.php | 16 ++-- .../BookmarkImportTest.php | 81 +++++++++---------- 3 files changed, 65 insertions(+), 48 deletions(-) diff --git a/application/History.php b/application/History.php index 116b9264..5e3b1b72 100644 --- a/application/History.php +++ b/application/History.php @@ -16,6 +16,7 @@ * - UPDATED: link updated * - DELETED: link deleted * - SETTINGS: the settings have been updated through the UI. + * - IMPORT: bulk links import * * Note: new events are put at the beginning of the file and history array. */ @@ -41,6 +42,11 @@ class History */ const SETTINGS = 'SETTINGS'; + /** + * @var string Action key: a bulk import has been processed. + */ + const IMPORT = 'IMPORT'; + /** * @var string History file path. */ @@ -121,6 +127,16 @@ public function updateSettings() $this->addEvent(self::SETTINGS); } + /** + * Add Event: bulk import. + * + * Note: we don't store links add/update one by one since it can have a huge impact on performances. + */ + public function importLinks() + { + $this->addEvent(self::IMPORT); + } + /** * Save a new event and write it in the history file. * diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php index 2a10ff22..31796367 100644 --- a/application/NetscapeBookmarkUtils.php +++ b/application/NetscapeBookmarkUtils.php @@ -66,6 +66,7 @@ public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $in * @param int $importCount how many links were imported * @param int $overwriteCount how many links were overwritten * @param int $skipCount how many links were skipped + * @param int $duration how many seconds did the import take * * @return string Summary of the bookmark import status */ @@ -74,14 +75,16 @@ private static function importStatus( $filesize, $importCount=0, $overwriteCount=0, - $skipCount=0 + $skipCount=0, + $duration=0 ) { $status = 'File '.$filename.' ('.$filesize.' bytes) '; if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) { $status .= 'has an unknown file format. Nothing was imported.'; } else { - $status .= 'was successfully processed: '.$importCount.' links imported, '; + $status .= 'was successfully processed in '. $duration .' seconds: '; + $status .= $importCount.' links imported, '; $status .= $overwriteCount.' links overwritten, '; $status .= $skipCount.' links skipped.'; } @@ -101,6 +104,7 @@ private static function importStatus( */ public static function import($post, $files, $linkDb, $conf, $history) { + $start = time(); $filename = $files['filetoupload']['name']; $filesize = $files['filetoupload']['size']; $data = file_get_contents($files['filetoupload']['tmp_name']); @@ -184,7 +188,6 @@ public static function import($post, $files, $linkDb, $conf, $history) $linkDb[$existingLink['id']] = $newLink; $importCount++; $overwriteCount++; - $history->updateLink($newLink); continue; } @@ -196,16 +199,19 @@ public static function import($post, $files, $linkDb, $conf, $history) $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']); $linkDb[$newLink['id']] = $newLink; $importCount++; - $history->addLink($newLink); } $linkDb->save($conf->get('resource.page_cache')); + $history->importLinks(); + + $duration = time() - $start; return self::importStatus( $filename, $filesize, $importCount, $overwriteCount, - $skipCount + $skipCount, + $duration ); } } diff --git a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php index 5fc1d1e8..4961aa2c 100644 --- a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php +++ b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php @@ -132,8 +132,8 @@ public function testImportNoDoctype() public function testImportInternetExplorerEncoding() { $files = file2array('internet_explorer_encoding.htm'); - $this->assertEquals( - 'File internet_explorer_encoding.htm (356 bytes) was successfully processed:' + $this->assertStringMatchesFormat( + 'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:' .' 1 links imported, 0 links overwritten, 0 links skipped.', NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history) ); @@ -161,8 +161,8 @@ public function testImportInternetExplorerEncoding() public function testImportNested() { $files = file2array('netscape_nested.htm'); - $this->assertEquals( - 'File netscape_nested.htm (1337 bytes) was successfully processed:' + $this->assertStringMatchesFormat( + 'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:' .' 8 links imported, 0 links overwritten, 0 links skipped.', NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history) ); @@ -283,8 +283,8 @@ public function testImportNested() public function testImportDefaultPrivacyNoPost() { $files = file2array('netscape_basic.htm'); - $this->assertEquals( - 'File netscape_basic.htm (482 bytes) was successfully processed:' + $this->assertStringMatchesFormat( + 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' .' 2 links imported, 0 links overwritten, 0 links skipped.', NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history) ); @@ -328,8 +328,8 @@ public function testImportKeepPrivacy() { $post = array('privacy' => 'default'); $files = file2array('netscape_basic.htm'); - $this->assertEquals( - 'File netscape_basic.htm (482 bytes) was successfully processed:' + $this->assertStringMatchesFormat( + 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' .' 2 links imported, 0 links overwritten, 0 links skipped.', NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) ); @@ -372,8 +372,8 @@ public function testImportAsPublic() { $post = array('privacy' => 'public'); $files = file2array('netscape_basic.htm'); - $this->assertEquals( - 'File netscape_basic.htm (482 bytes) was successfully processed:' + $this->assertStringMatchesFormat( + 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' .' 2 links imported, 0 links overwritten, 0 links skipped.', NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) ); @@ -396,8 +396,8 @@ public function testImportAsPrivate() { $post = array('privacy' => 'private'); $files = file2array('netscape_basic.htm'); - $this->assertEquals( - 'File netscape_basic.htm (482 bytes) was successfully processed:' + $this->assertStringMatchesFormat( + 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' .' 2 links imported, 0 links overwritten, 0 links skipped.', NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) ); @@ -422,8 +422,8 @@ public function testOverwriteAsPublic() // import links as private $post = array('privacy' => 'private'); - $this->assertEquals( - 'File netscape_basic.htm (482 bytes) was successfully processed:' + $this->assertStringMatchesFormat( + 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' .' 2 links imported, 0 links overwritten, 0 links skipped.', NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) ); @@ -442,8 +442,8 @@ public function testOverwriteAsPublic() 'privacy' => 'public', 'overwrite' => 'true' ); - $this->assertEquals( - 'File netscape_basic.htm (482 bytes) was successfully processed:' + $this->assertStringMatchesFormat( + 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' .' 2 links imported, 2 links overwritten, 0 links skipped.', NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) ); @@ -468,8 +468,8 @@ public function testOverwriteAsPrivate() // import links as public $post = array('privacy' => 'public'); - $this->assertEquals( - 'File netscape_basic.htm (482 bytes) was successfully processed:' + $this->assertStringMatchesFormat( + 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' .' 2 links imported, 0 links overwritten, 0 links skipped.', NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) ); @@ -489,8 +489,8 @@ public function testOverwriteAsPrivate() 'privacy' => 'private', 'overwrite' => 'true' ); - $this->assertEquals( - 'File netscape_basic.htm (482 bytes) was successfully processed:' + $this->assertStringMatchesFormat( + 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' .' 2 links imported, 2 links overwritten, 0 links skipped.', NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) ); @@ -513,8 +513,8 @@ public function testSkipOverwrite() { $post = array('privacy' => 'public'); $files = file2array('netscape_basic.htm'); - $this->assertEquals( - 'File netscape_basic.htm (482 bytes) was successfully processed:' + $this->assertStringMatchesFormat( + 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' .' 2 links imported, 0 links overwritten, 0 links skipped.', NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) ); @@ -523,8 +523,8 @@ public function testSkipOverwrite() // re-import as private, DO NOT enable overwriting $post = array('privacy' => 'private'); - $this->assertEquals( - 'File netscape_basic.htm (482 bytes) was successfully processed:' + $this->assertStringMatchesFormat( + 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' .' 0 links imported, 0 links overwritten, 2 links skipped.', NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) ); @@ -542,8 +542,8 @@ public function testSetDefaultTags() 'default_tags' => 'tag1,tag2 tag3' ); $files = file2array('netscape_basic.htm'); - $this->assertEquals( - 'File netscape_basic.htm (482 bytes) was successfully processed:' + $this->assertStringMatchesFormat( + 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' .' 2 links imported, 0 links overwritten, 0 links skipped.', NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) ); @@ -569,8 +569,8 @@ public function testSanitizeDefaultTags() 'default_tags' => 'tag1&,tag2 "tag3"' ); $files = file2array('netscape_basic.htm'); - $this->assertEquals( - 'File netscape_basic.htm (482 bytes) was successfully processed:' + $this->assertStringMatchesFormat( + 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' .' 2 links imported, 0 links overwritten, 0 links skipped.', NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) ); @@ -594,8 +594,8 @@ public function testSanitizeDefaultTags() public function testImportSameDate() { $files = file2array('same_date.htm'); - $this->assertEquals( - 'File same_date.htm (453 bytes) was successfully processed:' + $this->assertStringMatchesFormat( + 'File same_date.htm (453 bytes) was successfully processed in %d seconds:' .' 3 links imported, 0 links overwritten, 0 links skipped.', NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf, $this->history) ); @@ -622,24 +622,19 @@ public function testImportCreateUpdateHistory() 'overwrite' => 'true', ]; $files = file2array('netscape_basic.htm'); - $nbLinks = 2; NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history); $history = $this->history->getHistory(); - $this->assertEquals($nbLinks, count($history)); - foreach ($history as $value) { - $this->assertEquals(History::CREATED, $value['event']); - $this->assertTrue(new DateTime('-5 seconds') < $value['datetime']); - $this->assertTrue(is_int($value['id'])); - } + $this->assertEquals(1, count($history)); + $this->assertEquals(History::IMPORT, $history[0]['event']); + $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']); // re-import as private, enable overwriting NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history); $history = $this->history->getHistory(); - $this->assertEquals($nbLinks * 2, count($history)); - for ($i = 0 ; $i < $nbLinks ; $i++) { - $this->assertEquals(History::UPDATED, $history[$i]['event']); - $this->assertTrue(new DateTime('-5 seconds') < $history[$i]['datetime']); - $this->assertTrue(is_int($history[$i]['id'])); - } + $this->assertEquals(2, count($history)); + $this->assertEquals(History::IMPORT, $history[0]['event']); + $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']); + $this->assertEquals(History::IMPORT, $history[1]['event']); + $this->assertTrue(new DateTime('-5 seconds') < $history[1]['datetime']); } } From e9619cc4f8717c1b8b66f60998cf8402bb983349 Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Wed, 11 Oct 2017 21:35:17 +0200 Subject: [PATCH 07/36] Add EditorConfig configuration EditorConfig allows specifying indentation, line feed and encoding properties according to the type of file being edited. Most editors support it out-of-the-box, or can benefit from it through a plugin. See: - http://editorconfig.org/ - https://github.com/editorconfig/editorconfig - https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties Signed-off-by: VirtualTam --- .editorconfig | 23 +++++++++++++++++++++++ .gitattributes | 1 + 2 files changed, 24 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..5abbd7bf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# EditorConfig: http://EditorConfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{html,xml}] +indent_size = 2 + +[*.php] +max_line_length = 100 + +[Dockerfile] +max_line_length = 80 + +[Makefile] +indent_style = tab diff --git a/.gitattributes b/.gitattributes index dd0e573c..93900602 100644 --- a/.gitattributes +++ b/.gitattributes @@ -24,6 +24,7 @@ Dockerfile text *.min.js binary # Exclude from Git archives +.editorconfig export-ignore .gitattributes export-ignore .github export-ignore .gitignore export-ignore From a93b620a35a9768e102de31f19552624f33a0ae0 Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Mon, 16 Oct 2017 19:38:33 +0200 Subject: [PATCH 08/36] EditorConfig: add .htaccess support Signed-off-by: VirtualTam --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 5abbd7bf..4a6589a2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,7 +10,7 @@ trim_trailing_whitespace = true indent_style = space indent_size = 4 -[*.{html,xml}] +[*.{htaccess,html,xml}] indent_size = 2 [*.php] From 710291b164421663b9b1cf1f866e957801ff83e0 Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Mon, 16 Oct 2017 19:39:16 +0200 Subject: [PATCH 09/36] Fix: enable access to data/user.css (Apache 2.2 & 2.4) Relates to https://github.com/shaarli/Shaarli/issues/872 Relates to https://github.com/shaarli/Shaarli/issues/993 Signed-off-by: VirtualTam --- data/.htaccess | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/data/.htaccess b/data/.htaccess index f601c1ee..1d49da37 100644 --- a/data/.htaccess +++ b/data/.htaccess @@ -1,10 +1,16 @@ = 2.4> - Require all denied + Require all denied + + Require all granted + - Allow from none - Deny from all + Allow from none + Deny from all + + Allow from all + From fab0f4e5766d519a37b497927812c6b5b38e51ed Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Sat, 21 Oct 2017 18:15:52 +0200 Subject: [PATCH 10/36] docker: add 'latest' image This implies the following changes: - `shaarli/shaarli:latest` will now point to the `latest` release - `shaarli/shaarli:master` will point to the `master` branch Signed-off-by: VirtualTam --- doc/md/docker/shaarli-images.md | 13 +++++++-- docker/alpine/Dockerfile.latest | 47 +++++++++++++++++++++++++++++++++ docker/alpine/IMAGE.md | 13 ++++++--- 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 docker/alpine/Dockerfile.latest diff --git a/doc/md/docker/shaarli-images.md b/doc/md/docker/shaarli-images.md index 6d108d21..1d19510a 100644 --- a/doc/md/docker/shaarli-images.md +++ b/doc/md/docker/shaarli-images.md @@ -5,14 +5,23 @@ The images can be found in the [`shaarli/shaarli`](https://hub.docker.com/r/shaa repository. ### Available image tags -- `latest`: master branch (tarball release) +- `latest`: latest branch (tarball release) +- `master`: master branch (tarball release) - `stable`: stable branch (tarball release) -All images rely on: +The `latest` and `master` images rely on: + +- [Alpine Linux](https://www.alpinelinux.org/) +- [PHP7-FPM](http://php-fpm.org/) +- [Nginx](http://nginx.org/) + +The `stable` image relies on: + - [Debian 8 Jessie](https://hub.docker.com/_/debian/) - [PHP5-FPM](http://php-fpm.org/) - [Nginx](http://nginx.org/) + ### Download from DockerHub ```bash $ docker pull shaarli/shaarli diff --git a/docker/alpine/Dockerfile.latest b/docker/alpine/Dockerfile.latest new file mode 100644 index 00000000..dd4a173c --- /dev/null +++ b/docker/alpine/Dockerfile.latest @@ -0,0 +1,47 @@ +FROM alpine:3.6 +MAINTAINER Shaarli Community + +RUN apk --update --no-cache add \ + ca-certificates \ + curl \ + nginx \ + php7 \ + php7-ctype \ + php7-curl \ + php7-fpm \ + php7-gd \ + php7-iconv \ + php7-intl \ + php7-json \ + php7-mbstring \ + php7-openssl \ + php7-phar \ + php7-session \ + php7-xml \ + php7-zlib \ + s6 + +COPY nginx.conf /etc/nginx/nginx.conf +COPY php-fpm.conf /etc/php7/php-fpm.conf +COPY services.d /etc/services.d + +RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \ + && rm -rf /etc/php7/php-fpm.d/www.conf \ + && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \ + && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini + + +WORKDIR /var/www +RUN curl -L https://github.com/shaarli/Shaarli/archive/latest.tar.gz | tar xzf - \ + && mv Shaarli-latest shaarli \ + && cd shaarli \ + && composer --prefer-dist --no-dev install \ + && rm -rf ~/.composer \ + && chown -R nginx:nginx . + +VOLUME /var/www/shaarli/data + +EXPOSE 80 + +ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"] +CMD [] diff --git a/docker/alpine/IMAGE.md b/docker/alpine/IMAGE.md index 6f827b35..a8952257 100644 --- a/docker/alpine/IMAGE.md +++ b/docker/alpine/IMAGE.md @@ -1,5 +1,10 @@ -## shaarli:latest -- [Debian 8 Jessie](https://hub.docker.com/_/debian/) -- [PHP5-FPM](http://php-fpm.org/) +## Alpine images +- [Alpine Linux](https://www.alpinelinux.org/) +- [PHP-FPM](http://php-fpm.org/) - [Nginx](http://nginx.org/) -- [Shaarli](https://github.com/shaarli/Shaarli) + +### `shaarli/shaarli:latest` +- [Shaarli](https://github.com/shaarli/Shaarli), `latest` branch + +### `shaarli/shaarli:master` +- [Shaarli](https://github.com/shaarli/Shaarli), `master` branch From cfcc38192aff2bcabafa5cc67f18a5026255c96d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 22 Oct 2017 12:50:04 +0200 Subject: [PATCH 11/36] Doc: mention Docker docs in the download & install page --- doc/md/Download-and-Installation.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/doc/md/Download-and-Installation.md b/doc/md/Download-and-Installation.md index e5e929ef..3453e8b4 100644 --- a/doc/md/Download-and-Installation.md +++ b/doc/md/Download-and-Installation.md @@ -4,11 +4,18 @@ Document Root (or directly at the document root). Also, please make sure your server meets the [requirements](Server-requirements) and is properly [configured](Server-configuration). -Several releases are available: +Multiple releases branches are available: + +- latest (last release) +- stable (previous major release) +- master (development) + +Using one of the following methods: - by downloading full release archives including all dependencies - by downloading Github archives - by cloning the Git repository +- using Docker: [see the documentation](docker/shaarli-images) --- @@ -28,13 +35,14 @@ $ unzip shaarli-v0.9.1-full.zip $ mv Shaarli /path/to/shaarli/ ``` -In most cases, download Shaarli from the [releases](https://github.com/shaarli/Shaarli/releases) page. Cloning using `git` or downloading Github branches as zip files requires additional steps (see below).| +In most cases, download Shaarli from the [releases](https://github.com/shaarli/Shaarli/releases) page. +Cloning using `git` or downloading Github branches as zip files requires additional steps (see below). ### Using git ``` $ mkdir -p /path/to/shaarli && cd /path/to/shaarli/ -$ git clone -b v0.9 https://github.com/shaarli/Shaarli.git . +$ git clone -b latest https://github.com/shaarli/Shaarli.git . $ composer install --no-dev --prefer-dist ``` From 12266213d098a53c5f005b9afcbbe62771fd580c Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 9 May 2017 18:12:15 +0200 Subject: [PATCH 12/36] Shaarli's translation * translation system and unit tests * Translations everywhere Dont use translation merge It is not available with PHP builtin gettext, so it would have lead to inconsistency. --- Makefile | 2 +- application/ApplicationUtils.php | 19 +- application/Cache.php | 2 +- application/FeedBuilder.php | 4 +- application/History.php | 4 +- application/Languages.php | 153 +- application/LinkDB.php | 20 +- application/LinkFilter.php | 8 +- application/NetscapeBookmarkUtils.php | 16 +- application/PageBuilder.php | 7 +- application/PluginManager.php | 7 +- application/Updater.php | 8 +- application/Utils.php | 17 +- application/config/ConfigJson.php | 15 +- application/config/ConfigManager.php | 6 +- application/config/ConfigPhp.php | 4 +- .../exception/MissingFieldConfigException.php | 2 +- .../exception/PluginConfigOrderException.php | 2 +- .../exception/UnauthorizedConfigException.php | 2 +- application/exceptions/IOException.php | 2 +- composer.json | 3 +- composer.lock | 217 ++- inc/languages/fr/LC_MESSAGES/shaarli.mo | Bin 0 -> 22943 bytes inc/languages/fr/LC_MESSAGES/shaarli.po | 1243 +++++++++++++++++ index.php | 78 +- plugins/TODO.md | 28 - plugins/addlink_toolbar/addlink_toolbar.php | 13 +- plugins/archiveorg/archiveorg.html | 6 +- plugins/archiveorg/archiveorg.php | 11 +- plugins/demo_plugin/demo_plugin.php | 9 + plugins/isso/isso.php | 17 +- plugins/markdown/help.html | 6 +- plugins/markdown/markdown.php | 21 +- plugins/piwik/piwik.php | 15 +- plugins/playvideos/playvideos.php | 13 +- plugins/pubsubhubbub/pubsubhubbub.php | 16 +- plugins/qrcode/qrcode.meta | 2 +- plugins/qrcode/qrcode.php | 9 + plugins/wallabag/wallabag.html | 6 +- plugins/wallabag/wallabag.php | 20 +- tests/LanguagesTest.php | 184 ++- tests/UtilsTest.php | 30 +- tests/bootstrap.php | 6 + tests/languages/bootstrap.php | 7 +- tests/languages/fr/LanguagesFrTest.php | 173 +++ tests/utils/languages/fr/LC_MESSAGES/test.mo | Bin 0 -> 456 bytes tests/utils/languages/fr/LC_MESSAGES/test.po | 19 + tpl/default/import.html | 8 +- tpl/default/linklist.html | 30 +- tpl/default/page.footer.html | 4 +- tpl/default/pluginsadmin.html | 4 +- 51 files changed, 2252 insertions(+), 246 deletions(-) create mode 100644 inc/languages/fr/LC_MESSAGES/shaarli.mo create mode 100644 inc/languages/fr/LC_MESSAGES/shaarli.po delete mode 100644 plugins/TODO.md create mode 100644 tests/bootstrap.php create mode 100644 tests/languages/fr/LanguagesFrTest.php create mode 100644 tests/utils/languages/fr/LC_MESSAGES/test.mo create mode 100644 tests/utils/languages/fr/LC_MESSAGES/test.po diff --git a/Makefile b/Makefile index 656c27b0..300f1d7f 100644 --- a/Makefile +++ b/Makefile @@ -135,7 +135,7 @@ test: @echo "PHPUNIT" @echo "-------" @mkdir -p sandbox coverage - @$(BIN)/phpunit --coverage-php coverage/main.cov --testsuite unit-tests + @$(BIN)/phpunit --coverage-php coverage/main.cov --bootstrap tests/bootstrap.php --testsuite unit-tests locale_test_%: @UT_LOCALE=$*.utf8 \ diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php index 5643f4a0..911873a0 100644 --- a/application/ApplicationUtils.php +++ b/application/ApplicationUtils.php @@ -149,12 +149,13 @@ public static function checkUpdate($currentVersion, public static function checkPHPVersion($minVersion, $curVersion) { if (version_compare($curVersion, $minVersion) < 0) { - throw new Exception( + $msg = t( 'Your PHP version is obsolete!' - .' Shaarli requires at least PHP '.$minVersion.', and thus cannot run.' - .' Your PHP version has known security vulnerabilities and should be' - .' updated as soon as possible.' + . ' Shaarli requires at least PHP %s, and thus cannot run.' + . ' Your PHP version has known security vulnerabilities and should be' + . ' updated as soon as possible.' ); + throw new Exception(sprintf($msg, $minVersion)); } } @@ -179,7 +180,7 @@ public static function checkResourcePermissions($conf) $rainTplDir.'/'.$conf->get('resource.theme'), ) as $path) { if (! is_readable(realpath($path))) { - $errors[] = '"'.$path.'" directory is not readable'; + $errors[] = '"'.$path.'" '. t('directory is not readable'); } } @@ -191,10 +192,10 @@ public static function checkResourcePermissions($conf) $conf->get('resource.raintpl_tmp'), ) as $path) { if (! is_readable(realpath($path))) { - $errors[] = '"'.$path.'" directory is not readable'; + $errors[] = '"'.$path.'" '. t('directory is not readable'); } if (! is_writable(realpath($path))) { - $errors[] = '"'.$path.'" directory is not writable'; + $errors[] = '"'.$path.'" '. t('directory is not writable'); } } @@ -212,10 +213,10 @@ public static function checkResourcePermissions($conf) } if (! is_readable(realpath($path))) { - $errors[] = '"'.$path.'" file is not readable'; + $errors[] = '"'.$path.'" '. t('file is not readable'); } if (! is_writable(realpath($path))) { - $errors[] = '"'.$path.'" file is not writable'; + $errors[] = '"'.$path.'" '. t('file is not writable'); } } diff --git a/application/Cache.php b/application/Cache.php index 5d050165..e5d43e61 100644 --- a/application/Cache.php +++ b/application/Cache.php @@ -13,7 +13,7 @@ function purgeCachedPages($pageCacheDir) { if (! is_dir($pageCacheDir)) { - $error = 'Cannot purge '.$pageCacheDir.': no directory'; + $error = sprintf(t('Cannot purge %s: no directory'), $pageCacheDir); error_log($error); return $error; } diff --git a/application/FeedBuilder.php b/application/FeedBuilder.php index 7377bcec..3cfaafb4 100644 --- a/application/FeedBuilder.php +++ b/application/FeedBuilder.php @@ -148,9 +148,9 @@ protected function buildItem($link, $pageaddr) $link['url'] = $pageaddr . $link['url']; } if ($this->usePermalinks === true) { - $permalink = 'Direct link'; + $permalink = ''. t('Direct link') .''; } else { - $permalink = 'Permalink'; + $permalink = ''. t('Permalink') .''; } $link['description'] = format_description($link['description'], '', $pageaddr); $link['description'] .= PHP_EOL .'
— '. $permalink; diff --git a/application/History.php b/application/History.php index 5e3b1b72..35ec016a 100644 --- a/application/History.php +++ b/application/History.php @@ -171,7 +171,7 @@ protected function check() } if (! is_writable($this->historyFilePath)) { - throw new Exception('History file isn\'t readable or writable'); + throw new Exception(t('History file isn\'t readable or writable')); } } @@ -182,7 +182,7 @@ protected function read() { $this->history = FileUtils::readFlatDB($this->historyFilePath, []); if ($this->history === false) { - throw new Exception('Could not parse history file'); + throw new Exception(t('Could not parse history file')); } } diff --git a/application/Languages.php b/application/Languages.php index c8b0a25a..4ba32f29 100644 --- a/application/Languages.php +++ b/application/Languages.php @@ -1,21 +1,150 @@ //LC_MESSAGES/.[po|mo] * - * @return String Text translated. + * Pros/cons: + * - gettext extension is faster + * - gettext is very system dependent (PHP extension, the locale must be installed, and web server reloaded) + * + * Settings: + * - translation.mode: + * - auto: use default setting (PHP implementation) + * - php: use PHP implementation + * - gettext: use gettext wrapper + * - translation.language: + * - auto: use autoLocale() and the language change according to user HTTP headers + * - fixed language: e.g. 'fr' + * - translation.extensions: + * - domain => translation_path: allow plugins and themes to extend the defaut extension + * The domain must be unique, and translation path must be relative, and contains the tree mentioned above. + * + * @package Shaarli */ -function t($text, $nText = '', $nb = 0) { - if (empty($nText)) { - return $text; +class Languages +{ + /** + * Core translations domain + */ + const DEFAULT_DOMAIN = 'shaarli'; + + /** + * @var TranslatorInterface + */ + protected $translator; + + /** + * @var string + */ + protected $language; + + /** + * @var ConfigManager + */ + protected $conf; + + /** + * Languages constructor. + * + * @param string $language lang determined by autoLocale(), can be override. + * @param ConfigManager $conf instance. + */ + public function __construct($language, $conf) + { + $this->conf = $conf; + $confLanguage = $this->conf->get('translation.language', 'auto'); + if ($confLanguage === 'auto' || ! $this->isValidLanguage($confLanguage)) { + $this->language = substr($language, 0, 5); + } else { + $this->language = $confLanguage; + } + + if (! extension_loaded('gettext') + || in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php']) + ) { + $this->initPhpTranslator(); + } else { + $this->initGettextTranslator(); + } + + // Register default functions (e.g. '__()') to use our Translator + $this->translator->register(); + } + + /** + * Initialize the translator using php gettext extension (gettext dependency act as a wrapper). + */ + protected function initGettextTranslator () + { + $this->translator = new GettextTranslator(); + $this->translator->setLanguage($this->language); + $this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages'); + + foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) { + if ($domain !== self::DEFAULT_DOMAIN) { + $this->translator->loadDomain($domain, $translationPath, false); + } + } + } + + /** + * Initialize the translator using a PHP implementation of gettext. + * + * Note that if language po file doesn't exist, errors are ignored (e.g. not installed language). + */ + protected function initPhpTranslator() + { + $this->translator = new Translator(); + $translations = new Translations(); + // Core translations + try { + /** @var Translations $translations */ + $translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po'); + $translations->setDomain('shaarli'); + $this->translator->loadTranslations($translations); + } catch (\InvalidArgumentException $e) {} + + + // Extension translations (plugins, themes, etc.). + foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) { + if ($domain === self::DEFAULT_DOMAIN) { + continue; + } + + try { + /** @var Translations $extension */ + $extension = Translations::fromPoFile($translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po'); + $extension->setDomain($domain); + $this->translator->loadTranslations($extension); + } catch (\InvalidArgumentException $e) {} + } + } + + /** + * Checks if a language string is valid. + * + * @param string $language e.g. 'fr' or 'en_US' + * + * @return bool true if valid, false otherwise + */ + protected function isValidLanguage($language) + { + return preg_match('/^[a-z]{2}(_[A-Z]{2})?/', $language) === 1; } - $actualForm = $nb > 1 ? $nText : $text; - return sprintf($actualForm, $nb); } diff --git a/application/LinkDB.php b/application/LinkDB.php index 22c1f0ab..f026a041 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php @@ -133,16 +133,16 @@ public function offsetSet($offset, $value) { // TODO: use exceptions instead of "die" if (!$this->loggedIn) { - die('You are not authorized to add a link.'); + die(t('You are not authorized to add a link.')); } if (!isset($value['id']) || empty($value['url'])) { - die('Internal Error: A link should always have an id and URL.'); + die(t('Internal Error: A link should always have an id and URL.')); } if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) { - die('You must specify an integer as a key.'); + die(t('You must specify an integer as a key.')); } if ($offset !== null && $offset !== $value['id']) { - die('Array offset and link ID must be equal.'); + die(t('Array offset and link ID must be equal.')); } // If the link exists, we reuse the real offset, otherwise new entry @@ -248,13 +248,13 @@ private function check() $this->links = array(); $link = array( 'id' => 1, - 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', + 'title'=> t('The personal, minimalist, super-fast, database free, bookmarking service'), 'url'=>'https://shaarli.readthedocs.io', - 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login. + 'description'=>t('Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login. -To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page. +To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page. -You use the community supported version of the original Shaarli project, by Sebastien Sauvage.', +You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'), 'private'=>0, 'created'=> new DateTime(), 'tags'=>'opensource software' @@ -264,9 +264,9 @@ private function check() $link = array( 'id' => 0, - 'title'=>'My secret stuff... - Pastebin.com', + 'title'=> t('My secret stuff... - Pastebin.com'), 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', - 'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.', + 'description'=> t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'), 'private'=>1, 'created'=> new DateTime('1 minute ago'), 'tags'=>'secretstuff', diff --git a/application/LinkFilter.php b/application/LinkFilter.php index 99ecd1e2..12376e27 100644 --- a/application/LinkFilter.php +++ b/application/LinkFilter.php @@ -444,5 +444,11 @@ public static function tagsStrToArray($tags, $casesensitive) class LinkNotFoundException extends Exception { - protected $message = 'The link you are trying to reach does not exist or has been deleted.'; + /** + * LinkNotFoundException constructor. + */ + public function __construct() + { + $this->message = t('The link you are trying to reach does not exist or has been deleted.'); + } } diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php index 31796367..31a14537 100644 --- a/application/NetscapeBookmarkUtils.php +++ b/application/NetscapeBookmarkUtils.php @@ -32,11 +32,11 @@ public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $in { // see tpl/export.html for possible values if (! in_array($selection, array('all', 'public', 'private'))) { - throw new Exception('Invalid export selection: "'.$selection.'"'); + throw new Exception(t('Invalid export selection:') .' "'.$selection.'"'); } $bookmarkLinks = array(); - +7 foreach ($linkDb as $link) { if ($link['private'] != 0 && $selection == 'public') { continue; @@ -79,14 +79,14 @@ private static function importStatus( $duration=0 ) { - $status = 'File '.$filename.' ('.$filesize.' bytes) '; + $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize); if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) { - $status .= 'has an unknown file format. Nothing was imported.'; + $status .= t('has an unknown file format. Nothing was imported.'); } else { - $status .= 'was successfully processed in '. $duration .' seconds: '; - $status .= $importCount.' links imported, '; - $status .= $overwriteCount.' links overwritten, '; - $status .= $skipCount.' links skipped.'; + $status .= vsprintf( + t('was successfully processed in %d seconds: %d links imported, %d links overwritten, %d links skipped.'), + [$duration, $importCount, $overwriteCount, $skipCount] + ); } return $status; } diff --git a/application/PageBuilder.php b/application/PageBuilder.php index 291860ad..af290671 100644 --- a/application/PageBuilder.php +++ b/application/PageBuilder.php @@ -159,9 +159,12 @@ public function renderPage($page) * * @param string $message A messate to display what is not found */ - public function render404($message = 'The page you are trying to reach does not exist or has been deleted.') + public function render404($message = '') { - header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); + if (empty($message)) { + $message = t('The page you are trying to reach does not exist or has been deleted.'); + } + header($_SERVER['SERVER_PROTOCOL'] .' '. t('404 Not Found')); $this->tpl->assign('error_message', $message); $this->renderPage('404'); } diff --git a/application/PluginManager.php b/application/PluginManager.php index 59ece4fa..cf603845 100644 --- a/application/PluginManager.php +++ b/application/PluginManager.php @@ -188,6 +188,9 @@ public function getPluginsMeta() $metaData[$plugin] = parse_ini_file($metaFile); $metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins); + if (isset($metaData[$plugin]['description'])) { + $metaData[$plugin]['description'] = t($metaData[$plugin]['description']); + } // Read parameters and format them into an array. if (isset($metaData[$plugin]['parameters'])) { $params = explode(';', $metaData[$plugin]['parameters']); @@ -203,7 +206,7 @@ public function getPluginsMeta() $metaData[$plugin]['parameters'][$param]['value'] = ''; // Optional parameter description in parameter.PARAM_NAME= if (isset($metaData[$plugin]['parameter.'. $param])) { - $metaData[$plugin]['parameters'][$param]['desc'] = $metaData[$plugin]['parameter.'. $param]; + $metaData[$plugin]['parameters'][$param]['desc'] = t($metaData[$plugin]['parameter.'. $param]); } } } @@ -237,6 +240,6 @@ class PluginFileNotFoundException extends Exception */ public function __construct($pluginName) { - $this->message = 'Plugin "'. $pluginName .'" files not found.'; + $this->message = sprintf(t('Plugin "%s" files not found.'), $pluginName); } } diff --git a/application/Updater.php b/application/Updater.php index 72b2def0..723a7a81 100644 --- a/application/Updater.php +++ b/application/Updater.php @@ -73,7 +73,7 @@ public function update() } if ($this->methods === null) { - throw new UpdaterException('Couldn\'t retrieve Updater class methods.'); + throw new UpdaterException(t('Couldn\'t retrieve Updater class methods.')); } foreach ($this->methods as $method) { @@ -482,7 +482,7 @@ private function buildMessage($message) } if (! empty($this->method)) { - $out .= 'An error occurred while running the update '. $this->method . PHP_EOL; + $out .= t('An error occurred while running the update ') . $this->method . PHP_EOL; } if (! empty($this->previous)) { @@ -522,11 +522,11 @@ function read_updates_file($updatesFilepath) function write_updates_file($updatesFilepath, $updates) { if (empty($updatesFilepath)) { - throw new Exception('Updates file path is not set, can\'t write updates.'); + throw new Exception(t('Updates file path is not set, can\'t write updates.')); } $res = file_put_contents($updatesFilepath, implode(';', $updates)); if ($res === false) { - throw new Exception('Unable to write updates in '. $updatesFilepath . '.'); + throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.')); } } diff --git a/application/Utils.php b/application/Utils.php index 4a2f5561..27eaafc5 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -452,7 +452,7 @@ function get_max_upload_size($limitPost, $limitUpload, $format = true) */ function alphabetical_sort(&$data, $reverse = false, $byKeys = false) { - $callback = function($a, $b) use ($reverse) { + $callback = function ($a, $b) use ($reverse) { // Collator is part of PHP intl. if (class_exists('Collator')) { $collator = new Collator(setlocale(LC_COLLATE, 0)); @@ -470,3 +470,18 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false) usort($data, $callback); } } + +/** + * Wrapper function for translation which match the API + * of gettext()/_() and ngettext(). + * + * @param string $text Text to translate. + * @param string $nText The plural message ID. + * @param int $nb The number of items for plural forms. + * @param string $domain The domain where the translation is stored (default: shaarli). + * + * @return String Text translated. + */ +function t($text, $nText = '', $nb = 1, $domain = 'shaarli') { + return dn__($domain, $text, $nText, $nb); +} diff --git a/application/config/ConfigJson.php b/application/config/ConfigJson.php index 9ef2ef56..8c8d5610 100644 --- a/application/config/ConfigJson.php +++ b/application/config/ConfigJson.php @@ -22,10 +22,15 @@ public function read($filepath) $data = json_decode($data, true); if ($data === null) { $errorCode = json_last_error(); - $error = 'An error occurred while parsing JSON configuration file ('. $filepath .'): error code #'; - $error .= $errorCode. '
' . json_last_error_msg() .''; + $error = sprintf( + 'An error occurred while parsing JSON configuration file (%s): error code #%d', + $filepath, + $errorCode + ); + $error .= '
' . json_last_error_msg() .''; if ($errorCode === JSON_ERROR_SYNTAX) { - $error .= '
Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as '; + $error .= '
'; + $error .= 'Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as '; $error .= 'jsonlint.com.'; } throw new \Exception($error); @@ -44,8 +49,8 @@ public function write($filepath, $conf) if (!file_put_contents($filepath, $data)) { throw new \IOException( $filepath, - 'Shaarli could not create the config file. - Please make sure Shaarli has the right to write in the folder is it installed in.' + t('Shaarli could not create the config file. '. + 'Please make sure Shaarli has the right to write in the folder is it installed in.') ); } } diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php index 7ff2fe67..9e4c9f63 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php @@ -132,7 +132,7 @@ public function get($setting, $default = '') public function set($setting, $value, $write = false, $isLoggedIn = false) { if (empty($setting) || ! is_string($setting)) { - throw new \Exception('Invalid setting key parameter. String expected, got: '. gettype($setting)); + throw new \Exception(t('Invalid setting key parameter. String expected, got: '). gettype($setting)); } // During the ConfigIO transition, map legacy settings to the new ones. @@ -339,6 +339,10 @@ protected function setDefaultValues() $this->setEmpty('redirector.url', ''); $this->setEmpty('redirector.encode_url', true); + $this->setEmpty('translation.language', 'auto'); + $this->setEmpty('translation.mode', 'php'); + $this->setEmpty('translation.extensions', []); + $this->setEmpty('plugins', array()); } diff --git a/application/config/ConfigPhp.php b/application/config/ConfigPhp.php index 2633824d..2f66e8e0 100644 --- a/application/config/ConfigPhp.php +++ b/application/config/ConfigPhp.php @@ -118,8 +118,8 @@ public function write($filepath, $conf) ) { throw new \IOException( $filepath, - 'Shaarli could not create the config file. - Please make sure Shaarli has the right to write in the folder is it installed in.' + t('Shaarli could not create the config file. '. + 'Please make sure Shaarli has the right to write in the folder is it installed in.') ); } } diff --git a/application/config/exception/MissingFieldConfigException.php b/application/config/exception/MissingFieldConfigException.php index 6346c6a9..9e0a9359 100644 --- a/application/config/exception/MissingFieldConfigException.php +++ b/application/config/exception/MissingFieldConfigException.php @@ -18,6 +18,6 @@ class MissingFieldConfigException extends \Exception public function __construct($field) { $this->field = $field; - $this->message = 'Configuration value is required for '. $this->field; + $this->message = sprintf(t('Configuration value is required for %s'), $this->field); } } diff --git a/application/config/exception/PluginConfigOrderException.php b/application/config/exception/PluginConfigOrderException.php index f9d68750..f82ec26e 100644 --- a/application/config/exception/PluginConfigOrderException.php +++ b/application/config/exception/PluginConfigOrderException.php @@ -12,6 +12,6 @@ class PluginConfigOrderException extends \Exception */ public function __construct() { - $this->message = 'An error occurred while trying to save plugins loading order.'; + $this->message = t('An error occurred while trying to save plugins loading order.'); } } diff --git a/application/config/exception/UnauthorizedConfigException.php b/application/config/exception/UnauthorizedConfigException.php index 79672c1b..72311fae 100644 --- a/application/config/exception/UnauthorizedConfigException.php +++ b/application/config/exception/UnauthorizedConfigException.php @@ -13,6 +13,6 @@ class UnauthorizedConfigException extends \Exception */ public function __construct() { - $this->message = 'You are not authorized to alter config.'; + $this->message = t('You are not authorized to alter config.'); } } diff --git a/application/exceptions/IOException.php b/application/exceptions/IOException.php index b563b23d..18e46b77 100644 --- a/application/exceptions/IOException.php +++ b/application/exceptions/IOException.php @@ -16,7 +16,7 @@ class IOException extends Exception public function __construct($path, $message = '') { $this->path = $path; - $this->message = empty($message) ? 'Error accessing' : $message; + $this->message = empty($message) ? t('Error accessing') : $message; $this->message .= ' "' . $this->path .'"'; } } diff --git a/composer.json b/composer.json index afb8aca4..f331d6ca 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ "shaarli/netscape-bookmark-parser": "^2.0", "erusev/parsedown": "1.6", "slim/slim": "^3.0", - "pubsubhubbub/publisher": "dev-master" + "pubsubhubbub/publisher": "dev-master", + "gettext/gettext": "^4.4" }, "require-dev": { "phpmd/phpmd" : "@stable", diff --git a/composer.lock b/composer.lock index 435d6a88..ea20025d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "68beedbfa104c788029b079800cfd6e8", + "content-hash": "13b7e1e474fe9264b098ba86face0feb", "packages": [ { "name": "container-interop/container-interop", @@ -76,6 +76,129 @@ ], "time": "2015-10-04T16:44:32+00:00" }, + { + "name": "gettext/gettext", + "version": "v4.4.3", + "source": { + "type": "git", + "url": "https://github.com/oscarotero/Gettext.git", + "reference": "4f57f004635cc6311a20815ebfdc0757cb337113" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/oscarotero/Gettext/zipball/4f57f004635cc6311a20815ebfdc0757cb337113", + "reference": "4f57f004635cc6311a20815ebfdc0757cb337113", + "shasum": "" + }, + "require": { + "gettext/languages": "^2.3", + "php": ">=5.4.0" + }, + "require-dev": { + "illuminate/view": "*", + "phpunit/phpunit": "^4.8|^5.7", + "squizlabs/php_codesniffer": "^3.0", + "symfony/yaml": "~2", + "twig/extensions": "*", + "twig/twig": "^1.31|^2.0" + }, + "suggest": { + "illuminate/view": "Is necessary if you want to use the Blade extractor", + "symfony/yaml": "Is necessary if you want to use the Yaml extractor/generator", + "twig/extensions": "Is necessary if you want to use the Twig extractor", + "twig/twig": "Is necessary if you want to use the Twig extractor" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gettext\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oscar Otero", + "email": "oom@oscarotero.com", + "homepage": "http://oscarotero.com", + "role": "Developer" + } + ], + "description": "PHP gettext manager", + "homepage": "https://github.com/oscarotero/Gettext", + "keywords": [ + "JS", + "gettext", + "i18n", + "mo", + "po", + "translation" + ], + "time": "2017-08-09T16:59:46+00:00" + }, + { + "name": "gettext/languages", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/mlocati/cldr-to-gettext-plural-rules.git", + "reference": "49c39e51569963cc917a924b489e7025bfb9d8c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mlocati/cldr-to-gettext-plural-rules/zipball/49c39e51569963cc917a924b489e7025bfb9d8c7", + "reference": "49c39e51569963cc917a924b489e7025bfb9d8c7", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^4" + }, + "bin": [ + "bin/export-plural-rules", + "bin/export-plural-rules.php" + ], + "type": "library", + "autoload": { + "psr-4": { + "Gettext\\Languages\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michele Locati", + "email": "mlocati@gmail.com", + "role": "Developer" + } + ], + "description": "gettext languages with plural rules", + "homepage": "https://github.com/mlocati/cldr-to-gettext-plural-rules", + "keywords": [ + "cldr", + "i18n", + "internationalization", + "l10n", + "language", + "languages", + "localization", + "php", + "plural", + "plural rules", + "plurals", + "translate", + "translations", + "unicode" + ], + "time": "2017-03-23T17:02:28+00:00" + }, { "name": "katzgrau/klogger", "version": "1.2.1", @@ -686,16 +809,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.2.1", + "version": "3.2.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "183824db76118b9dddffc7e522b91fa175f75119" + "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/183824db76118b9dddffc7e522b91fa175f75119", - "reference": "183824db76118b9dddffc7e522b91fa175f75119", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157", + "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157", "shasum": "" }, "require": { @@ -727,7 +850,7 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-08-04T20:55:59+00:00" + "time": "2017-08-08T06:39:58+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -1875,20 +1998,20 @@ }, { "name": "symfony/config", - "version": "v3.3.6", + "version": "v3.3.8", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297" + "reference": "6ac0cc1f047c1dbc058fc25b7a4d91b068ed4488" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/54ee12b0dd60f294132cabae6f5da9573d2e5297", - "reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297", + "url": "https://api.github.com/repos/symfony/config/zipball/6ac0cc1f047c1dbc058fc25b7a4d91b068ed4488", + "reference": "6ac0cc1f047c1dbc058fc25b7a4d91b068ed4488", "shasum": "" }, "require": { - "php": ">=5.5.9", + "php": "^5.5.9|>=7.0.8", "symfony/filesystem": "~2.8|~3.0" }, "conflict": { @@ -1933,20 +2056,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2017-07-19T07:37:29+00:00" + "time": "2017-08-03T08:59:45+00:00" }, { "name": "symfony/console", - "version": "v2.8.26", + "version": "v2.8.27", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd" + "reference": "c0807a2ca978e64d8945d373a9221a5c35d1a253" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/32a3c6b3398de5db8ed381f4ef92970c59c2fcdd", - "reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd", + "url": "https://api.github.com/repos/symfony/console/zipball/c0807a2ca978e64d8945d373a9221a5c35d1a253", + "reference": "c0807a2ca978e64d8945d373a9221a5c35d1a253", "shasum": "" }, "require": { @@ -1994,7 +2117,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-07-29T21:26:04+00:00" + "time": "2017-08-27T14:29:03+00:00" }, { "name": "symfony/debug", @@ -2055,20 +2178,20 @@ }, { "name": "symfony/dependency-injection", - "version": "v3.3.6", + "version": "v3.3.8", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "8d70987f991481e809c63681ffe8ce3f3fde68a0" + "reference": "2ac658972626c75cbde7b0067c84b988170a6907" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8d70987f991481e809c63681ffe8ce3f3fde68a0", - "reference": "8d70987f991481e809c63681ffe8ce3f3fde68a0", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/2ac658972626c75cbde7b0067c84b988170a6907", + "reference": "2ac658972626c75cbde7b0067c84b988170a6907", "shasum": "" }, "require": { - "php": ">=5.5.9", + "php": "^5.5.9|>=7.0.8", "psr/container": "^1.0" }, "conflict": { @@ -2121,24 +2244,24 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2017-07-28T15:27:31+00:00" + "time": "2017-08-28T22:20:37+00:00" }, { "name": "symfony/filesystem", - "version": "v3.3.6", + "version": "v3.3.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "427987eb4eed764c3b6e38d52a0f87989e010676" + "reference": "b32a0e5f928d0fa3d1dd03c78d020777e50c10cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676", - "reference": "427987eb4eed764c3b6e38d52a0f87989e010676", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b32a0e5f928d0fa3d1dd03c78d020777e50c10cb", + "reference": "b32a0e5f928d0fa3d1dd03c78d020777e50c10cb", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^5.5.9|>=7.0.8" }, "type": "library", "extra": { @@ -2170,24 +2293,24 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2017-07-11T07:17:58+00:00" + "time": "2017-07-29T21:54:42+00:00" }, { "name": "symfony/finder", - "version": "v3.3.6", + "version": "v3.3.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4" + "reference": "b2260dbc80f3c4198f903215f91a1ac7fe9fe09e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4", - "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4", + "url": "https://api.github.com/repos/symfony/finder/zipball/b2260dbc80f3c4198f903215f91a1ac7fe9fe09e", + "reference": "b2260dbc80f3c4198f903215f91a1ac7fe9fe09e", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^5.5.9|>=7.0.8" }, "type": "library", "extra": { @@ -2219,20 +2342,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-06-01T21:01:25+00:00" + "time": "2017-07-29T21:54:42+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "f29dca382a6485c3cbe6379f0c61230167681937" + "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f29dca382a6485c3cbe6379f0c61230167681937", - "reference": "f29dca382a6485c3cbe6379f0c61230167681937", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7c8fae0ac1d216eb54349e6a8baa57d515fe8803", + "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803", "shasum": "" }, "require": { @@ -2244,7 +2367,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -2278,24 +2401,24 @@ "portable", "shim" ], - "time": "2017-06-09T14:24:12+00:00" + "time": "2017-06-14T15:44:48+00:00" }, { "name": "symfony/yaml", - "version": "v3.3.6", + "version": "v3.3.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed" + "reference": "1d8c2a99c80862bdc3af94c1781bf70f86bccac0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/ddc23324e6cfe066f3dd34a37ff494fa80b617ed", - "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed", + "url": "https://api.github.com/repos/symfony/yaml/zipball/1d8c2a99c80862bdc3af94c1781bf70f86bccac0", + "reference": "1d8c2a99c80862bdc3af94c1781bf70f86bccac0", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^5.5.9|>=7.0.8" }, "require-dev": { "symfony/console": "~2.8|~3.0" @@ -2333,7 +2456,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-07-23T12:43:26+00:00" + "time": "2017-07-29T21:54:42+00:00" }, { "name": "theseer/fdomdocument", diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.mo b/inc/languages/fr/LC_MESSAGES/shaarli.mo new file mode 100644 index 0000000000000000000000000000000000000000..d6b195da781c5e93bc2f2db138f449939003614a GIT binary patch literal 22943 zcmdU%3zTJ5dFK!E5KDRa;Gx1C8k_2ts@o4hp=er)epXY}T~t+f3sLaYy{D=UeeXS& zbIz^qY89d;K4wfbW{epWMU#xQ5tPv+7*Quw2I8pEn1_!z1R3HaIx%Y;6XPTq=lB2i zKIh)*ZXT|zm9^$BcKy$DpS{2Rz4zBV^4yc&9q{`e?Zvd>nL%*n2|@71({(inKK{HQ zcs_U^$bZ2@{CO7mP4EGpdmNnN{DUtDf-Aruf&<|2*+FnBcm;SRI0~K#zSduVJ9rA` z?*@m!_kkyZp9e1nzXZMn{2{m%Jnx(!cn)|II1U~FF9GiduLd6n2f!p!&TN)OUXk)Oha$pAFs* zUI_laKmQKMk_A5iMUVdjs-KHr6a*W=mxJo(7Vy>JA@F+e&%hhNH4q8X1#bj3zdJy! z+Xp~>_ij+@@G($y`V&y&e%zlAKwORYg`n<-;KSg0Q1AbPzy1^OCeF{ez&+OnMaSDf zjqhEc=yDgR`TQ2B@q7l;h0zK?_Aljr=KAUF#=6V&xppy<8}ycC=P_5HVk8rM6( zec%T`{tNz|KXetG9Xh_c9MpHO1@-(ckfjT51g`)Ke|`^mIp+_9uLl1aya@am2Dt;g z9lR2J7!<#qu*%WrOi<%F8`L_U59&K>K}Zu^2_ibdeo*skf#RQALCx!}pw{JGpy=^l z(CFi@KL|=bzTnTl4vN0t2A>Q59jNh~%3!sA=YU$bt3j4B*aE8m*MRz70;<2<!E5Wls&GQye{oV>{UcU%xeSaMkJwFAi{~v&&`%gg0&oddE z-VZ^IXAl%WuLq@9MnQdV+@HS^)bn+ap#-;p8&BYS;7OeS-o@_yhd@2|HBj$A0&1S$ z1+@-81ogd>V5U>R(?Idp^Fcj-G5A_=BdB@b3u;^sfY*Xw0`>lxYux&s2de+`L0umN z#c%6D(Pa;)@ijqx=T`7`@J--W@Wi#y0Ne@cJOOut_kt&bKLYFEKZB=&(M#R+TRe8a z6S=0c-qGzc+$fr#dKp>VN~_&w-l%J3#U8 zUEnPES?~hrf8mf@_iKjT`};taB&dUsDtH@sHFzI*9r(BY{#7Vt$!P@Y{{7%5z`qAY zzmKeQeDZlv^7)v@e*#69v)4O5d$Gq2py;q2+yL(K*Ix%}Uccn;zaJDmJ_KrAJ_?Et z?gLS&!TsRr;2-B)Zj_be zd>NE{d=uRE4CnziIlug6jt(CJpU3&5;F;hLLGi=MFL(U38`O9YfLfmn)VN*`YMj3d z!s5Z_K!zH82bBIl2Vp!7ycL`SZwJNC-vM`lYf)w#1_j8lgL^>n&&NRV%NIb6TgLL4 zzze`@!1F=z`Ar^Q3*N~2uYxy#{~J_46IVHUC7_;9{rTI$kn?we;+s!^n%6hLv%$wf zt=B(-uugC?lNBFc3c`B9^`Q9YHc)hX0Mzqe0i_SW>CgWL)Vll#l>Ru8mqd?2P}eU9 zxfP6p=YqF^F9qKN>bVEO1K?M{*MOT~0^yyY`uo42g6uxzjy)Ogw; zED^i`6g@uyYCZn|Tnl~~d<^_CsQ$injgzmx1UsDnqd#wsI(~a2cm~(+1kVHS0rlO7 zJ$@M!AAAjzJv)^}e+IZ2)I3MQ9pH_i==UK|eDK?#zW=wN^!h)5`u^!#+&bOv@i#!t z>kFXh{s?#u_*dYIKq)mo9lRL40K5qlKffM)9rzIV5^&2lXJ;3{>vSEI96oou>-R!X za(oG>`Cb8PygNbh&8t9t?~ULy!1sgagC7Ld&*wmm?+?L?z^{WRg8u|+{Y3=L^Esft zvkKIB*MTntcY|kw35ZArhryG<`#^o?cR;=Or=Y&`m_PrY9)Ild6ej-`?mrJyfA0m) z0zV3N!Owzv|FSXX-)siee+Ir7d?$Dfcn_%ezYm@Q{sh$epMo$y6AZyq!8M@fu>sUP zCqcdUDo}K3fqL$Bpy=}R;OGfKa0Jxz-(xYDF@-sQcG~8poK& z32+VP*MmdgVenbthrtGTAE^G$zTU}C9n`wM4^;p6f|B>ofSTu5LDB6Ia25D>;2Xem zr--wl{~h2k=YKQp=>4J@7sp%zN~(rK0a>zO8T>MM#y+>c ze*uc0Pu%bNy8v9z`2;9>-UhxDd=sd7-s`XbIjHeG3cdh5{grNBF99WQmx7|l%RtfZ z8h^e66#rZgiVg?-{llQX_Xbeod^3p12VVsxSLfc~?8pV6=(Ggt{da_r z_MM!;aZJ;@KTErZ1}Upwi=+QcYtrte{af0bX&<3|k_KDYya4l?9sxf~)77{1xv{{eoCCf=;ko=9^p({)!j% z)0*5xyOfsGE~ot$ntpGiJw*E|?M&K#qv>}s?PWCajec_uEY8;99sc|rP(1cH?NhY> zNLxoMf0Co$rJYE-vAhTl`{S+PFVbKYvt1{Fe?+^Db_?w?nt1Ur?M<|=)AW0ccB>t^ z*zlMAaor;(H~2%^pZoIi{Ky74*2uG1pkV**`Iej zei?kbKi&d<#vcog(7r|cE!u;$chdBe?q9Mamxu9(2G8*)e+_<`_6>h7dpJY8iFP|} zg0`1-J+1s*z`-xkE~K6AFa9TRmi8*zLE3&=LeuYeXc_JEv@g)Eq`iywFipR!9hj}y z#qlQECukp~y@mE)Y44>?()xd||2!DdcGLcXR{o^__W6_Z!QY^L zoc165wM#q-=4sFN=c~crraeG=EA6|qh^F7`X;;t=(f*Y7A=;nNMrryzN_!*iS7?7m z)9)(Uf2XabJ(qSr?M_;k_UknL{=EaUxhco5rG1Fr!8AJ% z*3&GD>qT=ZOxxj7+ReC~Eyh`G(`>eRB03OM~uXkRC`NMv-RGJoHGzxhQGY!u>Ie zQcqj0n7KXiFp^?05633MsL@~zbA5Vyx5?zL??%mJE{PlA*0dEtwA~EVW*LlzjkuMD zoo08Q>-CgX(F~(zGh|2%OyioPry_23nh|tt#CbB`h9X93|6(IvjGHOEk=N+bU$j+V zFnKtc983;`MHbZ$sP}r>u-Q#7L{ZjE;G_9iFHVlJzV$3Fg3(4J+?^J&9Yo=5wFw zQP=6d!;0`7UDTCkk{32OdTTb->@t*{<1t7PEon?qm@^7^R`ak&!*+boKN`W1LP85vi~AARkIfF@oFXii3KZ*@*Ndaq_MvmWQo$HsKplr_)U8QGu}0NoOINjSJ2h zT|11kjFn33#@3DS;DTO2NHx50*Yuv81`^CJyH^q)F?v(47re=fRe!Ew$6>W(dyed;}#FHWF!_G+W{>ujFw% zPl{wQ4z?^r?fDXVgpP}Xf$?sU6wP=DXvK$F=bNWos^vrX#TBAHSeb#p|uEXgI zsH$YnRM0&0Y9u-8sm*ARL6*qf%KC-1ZetQ_6>*!st8P3yxXFZjr^aEqyoezNT!_TA zkZoh$a4wD;P{Lkyi^ExhhTa0lFsKv>TneLkGyt<{rd{S>O0(pL#i-ehrDZdS4)Gc$ z`J(QM#}2}cTG9r+_!|(jJaXF6bFVH#RCFvy-DbmVgE*3fL%3KQW5WKMXiW{(zFNJ} zokbQGbbYcrJKde#*`1y3&W==1wFe6ZU1SM*DBNp|nT7QxOBJ?aG;ITgu*E6+-U4n# zxh@4;y^gTSbJmPeJ6q$to+TZVvcBW+V1f>SDX|)GF9ebv54IYk8Rv4G%YKyz*O_4~ zb^Ahci&ui34Ex z>_g!+!&VH4sVI1acObV{p-|8mb{139iZXgFwJB_wCG!i#aNW2ianiWzTK`};Bc2{- zAgcoF-FVzs@2Bna*e;)Tk3*84;|#ODkfu!Xcs+NWzQl~pq6LtewuC6y)<_Eb6E{Y} zU|ZW3&A|#DGpRza7z3gnYW~@9YTNWoh{^KjLlScHT%^S6l{7e8nER)COT10GI^MQder&**2uz}2}v=zldmUibC!X2z0gz8!+ z;u&wB#UOYzQMcrvBnTheR$BJvPSQxu{)Rv4T%srFB#Kdk#!sgOLL*W)? z*g_-kLGwk>Jce15w42vzwY6pUm?ihQ?Xqupv};y3u)Rf`uMGpP70ee?j8jmal?K}6 z%wNod?P;^YpDc_cMx}c=fp`^Bc>UBCxiAUKTs!U)?Qk@!FJNnGX*Tbl+mT!w?8G^7 z%v5(m&6S=SM5>)4iyFpBd|JM%vF1)WqZ&hilN2Q|?e0Cb@CJNb`3$p|KPY968q3(? z;dW6Accfu-Fj}ewV@694z4dUH!zWx16n$(-!pWZt7ZsErTjvCp*9Df@F6g448?g9h& zEa3|BibjA)ZEOG++?3JWO{bsLrMrWVA{QY%5EC&(8D2D>s)f@S9?ts! zrC=N%W--Bgf+JlVV&N5WoIH3`D@wI;MRGHWp~lCXX1TZry>Pk4Q^E}WCH4s>qPB%O za?U5PMUC_zLDv#C?9kEYL`nVTZ6Z39w7M<)?!$P*a;4GY!2|*}k;>n8W;BczvG686 z6Y?rBoQ+_@TT>mlV1sxIPC|i%!{KCvYc-31#4H0vNx^PBc|~@1P`P=#<3o6Lgu%P3 z753^Mr>J)=HnU=vFqg2%T-t5B(`0T5*90FuRsf-RA%owfkj?*!8cV?*dC0*YLNXV@ z#)sem^-^iffblwKbh2~=gUO^`D4O06U8<9V?nF_TjGv3gJa-+JH7CdRk6m}OyiJLR zJDVPx*@k2OG!ZMC+j&*ZV%@eV>#I1T*@vsZ%74W*?ZLK#k zkqcu`4RR9GCW!+V9nrBOSP$W<)%hy(ES;O=$?l#(hxdz9`AW(0&VicD%;L()f}cv_ zde%1Ar?k0d6x;Qf*xN{(#fvgX#vPL`@C!UTmERu*;>}9qDZR2C@7)f(r|;C?R9r?e z!BpIeTeC7{0}&M(rs zB8%g7EeUtF&V4H9)P4J+g`(9Qb{;+BTD5s<+vwJbZJX9bn`^-|agP0w|05~FvzQO2 z;|TNafz-&Mf@5zjIC47NrNMN^#g%~#hTcqPdZE|ClvjGkT6QE@vu`#c?kctV*4lMrFnIarx9VvJ&@VhV^ z8*DN7ewT^6WSMT*v)5cVQh&}dkss%o5za(h`2rpt{67-RKt&ocn!->WDcNEN3NJI` zZ|`)h_JgSP-4?T(XSm{K9?U=`i|ax^8M|VM4z!K=O1-=Up#jMVk%C}s7PedpXA=S{ z++m_|bTe5*ll6n~thcn9lQM9f!V)LdDwJJezzE2jbWvaE60|X9MkE@VsfD5n!j1h? zW+}(Akzzo_ET-Qj!CJuC7{lPGhrv)MtXmbkYRr1YXI9+<5|4lJSI~8NiuW{x__Y(} z!mZ7-O{E$5+$xR z4+Oa)jDanXTY?>R)8j6CVT}Zg3~gSl6*rsJs8-kKXo4B#Y=RkVLLTg$8l#P?iJX`m zbS3%A0;eB)EnfhWtMuYtGtfO5@o5vOP3|?57AC9_xP$_EP?t|D7E1Ett__Lj$v!v} z?cc8jdokpeQQ)mIRaFHv>PNAQPfjuCS<1X5s%Tg_UnxPYS}7dl+CK~&qgw)ePGnJ!6q zTqkz_==k{PwWB-aG5l0s?EbjPx=TBh0()UNW7)IZEwuAh9f2x()WVrG#4=ixTd9~K zV}8pUy0^%Vpwhm`Jgl+qp1W#inS+~r7aEmv^har?JE>H3tFZo zL+vt<7}mUOEpwtOdCQ8{2<@bv(T|aWF!xj&&q-r+TQw{6L#PdnuykT}&df|sS1ycR zaZ86Xo$IzO7l*zpj_TGq@!>9_3FqPT(ZdVIQ8{gfo>`KlTzx)}&gQB7vJ0!F_eoZ! zLX?JAT-YnCwbO*ljVps2<+9oYXl7AYb}B1zplvZzOvYb)x7cm6GZUG#lPR)}eB4Vy z#VIEOUedRzlmvD!C2A&JX@pda=E8}d1#;`&2$U~DKuWenW*w^srF+Uwh%-qTHOpQc zd#B1};2)4jGi`eGOZ^wC=kXf@%@z*8rrMP4Ij*-N(i~U`i>~B3luEW1cz%%3I!Qeb+=2%E-pz`WJjl&`^N`wVRo|d1~ACZByH(xP>{yfEV4IFvpbH@K|GbpR*++ zCC3a*?wJ|hB2N|zGfYA-9tk(D-|+I`^_LHCTpw<@bma2OE?K{E{dyi8o+5;jB|6S? zmyK-L;GP>tVuokPW#%Z1G#d#A#@W^BqBe(T;;7a8a(Q_KOMLV8sey^HiEX{{ZK$mu z*g~{Iq&YmZ)L~fiHV6_3U#^ncW3QsPdhg8k;VXL2>8oT-hqtxaF~egS39pzCge1~dgE0} zWpH@)ns&H3+_3hlUPsgDn0ma$jc+7$1FSxCUA&|{s(c*pjTwz>7}|K1#^*=j;+ILr zF}$9U3lNO@r-RYnJ|xjb5;K_Gl>&^Tc(m)` z>H$b|<%F%a5viSIr)HrNe2SB-yOaWST`j<9u0nvr;i3ZQTiJ_hM!kxF2Y=JLg)e$F z0pWw64r4T@_k-~|k*UV=5z=dHq&HY5`Xczlb;BXzqoiM*sVn!q$TVnw&%S1##n3D= zgeJ3SWNAWCKx1(E2)jYbs#r|Cg^Ylbf6I5PXR90_+$QT148lY^Lu4wO;@0LTDM#3}j{B}AVJ~Au2DM#FM;g}hkrtmW z^Phyqi%6g{K#WieMVSI*>K1spx!b$w$OPLOR^Mv-h6iKz1NQcdZ;ou}_%pCJU=?TnXqpP%JGo^qA#1eg$BU7;XG)B5YQW0Y`yz!5AX}q|=(c=+8x7e}^lc0E` zuJl`Tkkl=gw34J82o71B<&Q{nEFYO?A!Y&q!N@KpLBUQsPq0cH6WB(%VEz zu)V>)L}o zMM_2%yQK9q_E}Nx5y2pue#L#uNBRmsSVltB(wahjFR!}nD?jAc5aXa*Tpger8cb0c zp=oF1@p zk*a#6##T~~o4k1suJ5Kr(qQ$1eP~GpSN>3?frA9i=XYbpC=<2^G^HRadeD1Ir9#&mkHvpP4jl8bs_lTa{g#ixx>o z5d3zP2Q=0+pGp+TvEAq;{F%~V${Lh!JoxFL{1!f<4e$TI4~&bqQgVBuPdOyCV zV__U|pmLN>O|jDM067~8M*ZSDv~{-xp;vTXC74L`Gky6Ux?T>63RKEVWE)EcVD%t7 z3rw{v9<3Wm&wlhXjWMAy%r-NkPhzEDALkazFT?k6FCd1RC*M~23S}ppY$A}LwXWxhfY=Nql5PJC^WS3efe!UK89eCrBP7 z3g4wJj;g#^<6L}Kf53W`(|meW7b`wk4Fi2&s@c`95+>oFS)mJuC z3wESbZkm=77x^KXB;>MV<-7=xa5zvj8V>G<{5)lgT)nou54f}R@=!&bRRNu;UT0mJ z3T(P@(hPR**>_^95+k*Bw;~s&oS6=wmVPdbNdN6+1+w z@~}oq6``CiwhL8J+98V7@bWCYlXJ{UF5;Q!HZCl>OMwTflikGSBYx8rL3UEZSFBWr z{(Nv&`a4I6>xgm6`?U}^bopM#rgc0bT)exjlw}rIx{Hgc+;khSvKpz}1U^#+rdEJ6 z5>AwA22G)~g9Kh8Rk~OhxV{n}h3sYcXE7H6*@|)mWtgr@E~&mDCf zOjMk&qay3pnYI0c1=SxOQ$C~;A{P)VIa3|?M=8$=c1e}yUH*XpHPWMU##SUhN~_I) z$yFSEyEM@LRxgK#_i8&rK3pK!y|qj5u4@D~B8a>P1*6iIy(z2ah!}wK5)z=Q1~(0dRhFv`fQ$A~T`V3H^*%_{2#_T)m5D&yTbxmG5} zXQ%LaP-Lc~ED@;Zt*0Vxnq>AIFl?OmoGo!_{q}1gt8g+G3I$5l@1jC|<+Gkln@HTu z?JE^^f98Pny)@XN6|2tZv{5b{j0iUe{Zcla--POwJl@Zaq~OM$k?r@_jeTnm*r4sAdXeeBSq!nb1oE6-1M*9849D6 zG1Y@lT^hC5a)&7R-ujk`#)qpb!??-_uBeh4mB~I*8SaS1fIdr6*@U41c`ELfd?r)*A}C=tvzID1H?uadPi7NY|5vG$+SVp_ zi66!aP%u@dlu-$a3(GnK)pgQ* z82k3@o%W6@Yn{hbkl2nwLS?c!u`r2tG}t`Qj=p6mu;q-ALGUkTTmaxovMfg=kv6M4 z&T<;Vx-+}V#=B1kxxNU{c-;0DG3;a?S3X5q9YUyw#0I^`X0%_*vsFT+M~0qbUpXp1 zhb2SZUQEem88J!bs%XolJ2NTE3Ao^0y8IfphqqyJGulU(=3nf*CX+o3s{W^^=&^QD#ScLzL=R8?K$J~96< z{2HzUq{b{uHMQM7fYn~o@348H_-fdacD$nO%hIeaE2MwDp($3O%*?{_-4q{9iYLp| zju^JT(8~fk=OfCMfGH8aM&bn4?Tr|dmjbYTMUK}ANU%qC^Qe+8@}*>{m50Jo<7~@? z@`CMr95!7Pu9S&F?RRCE1{2&Bc~i%h!C2Euy_S!NPADfz-+WBiV@5u-B4ytTG`)rz zvf6{E{w6NHGd9k?Z{o`Nh=P(Hr2 z_4Qp!O;$UQ_@mle6nV92CW|_*f4%@Mo&OV<`Ou;sx0(!MRnCCYO{IztgMHq$gkmle zRozl$Syz>1K0RblREDkB#qs?>O}j6u+TT}m_S1(|*CKnw@8}54u+;4rhFd5$GBI@f z#uhq3QAbTLEVc-gev0qVsx8}w;34MLuVjZ@l%;=rg?<=}X4$WCm%PtHR(@c>dLcEa z0%Ta!$5h^3q*Fh%s!)x-SZ`B8gqAC6)oeGMT}JJODh6e5ZU85siedCB8|r>}e36<$ zAqcBG`gXUCUnYIchNQ|lRARTXW=)lgA))s%#cIbelo6yaSme% zG0yg&%eYwnrVQ1jIeO_OoBC5dUM-2us?X;=&Wm;I78CBhq7{|hgDl#?wq4JXK}=zD zOC~krU&h#|En(@bqDvJ;2=m>F5YrB?qkU|*eGtl0Ayg2Qxjm~s<0H4kZPfJVldN~g zKTb~J2ZUICaAd4vdwqT!M?Kj$<|n9tGtF7`>~_P-5)Hx`_+UlPrPtY43THj=$1*fM z5ank>fd=N0=4Qm?VjW6l*1;Cs?sHeX(UWr{0|x|m||nc3N{F4&75PB z>RPG(+7dZF@~)2hlDY6!fM^~ZqR%H(IVX2ZGN}|T9A74&++N5EtyGWMc1|IMvHtN=mrwU`Gj=Lg3F1Q}IrvEF>h?_5adV?j z=h0s0TKOOnJNjI<_V*xREu4~DBcI!egtJOwC1=-;%E1RmWgrfuWlbJA1)oi*OhnM< z{MvCZN1Y#Amk0IDbG5yKelG=@IE4n7(?& zdcCrpr|L%4daTdU6mmhf>i48Qe8ry9WhDvW{L+QC6m|py_g~|G%#q5Cs09@3X&Ov) QMYW!b;$eIh>=nWP17x~i3jhEB literal 0 HcmV?d00001 diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po new file mode 100644 index 00000000..8763581b --- /dev/null +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po @@ -0,0 +1,1243 @@ +msgid "" +msgstr "" +"Project-Id-Version: Shaarli\n" +"POT-Creation-Date: 2017-05-20 13:54+0200\n" +"PO-Revision-Date: 2017-05-20 14:11+0200\n" +"Last-Translator: \n" +"Language-Team: Shaarli\n" +"Language: fr_FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.0.1\n" +"X-Poedit-Basepath: ../../../..\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Poedit-SourceCharset: UTF-8\n" +"X-Poedit-KeywordsList: t:1,2;t\n" +"X-Poedit-SearchPath-0: .\n" + +#: application/ApplicationUtils.php:152 +#, php-format +msgid "" +"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " +"cannot run. Your PHP version has known security vulnerabilities and should " +"be updated as soon as possible." +msgstr "" +"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne " +"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités " +"connues et devrait être mise à jour au plus tôt." + +#: application/ApplicationUtils.php:180 application/ApplicationUtils.php:192 +msgid "directory is not readable" +msgstr "le répertoire n'est pas accessible en lecture" + +#: application/ApplicationUtils.php:195 +msgid "directory is not writable" +msgstr "le répertoire n'est pas accessible en écriture" + +#: application/ApplicationUtils.php:213 +msgid "file is not readable" +msgstr "le fichier n'est pas accessible en lecture" + +#: application/ApplicationUtils.php:216 +msgid "file is not writable" +msgstr "le fichier n'est pas accessible en écriture" + +#: application/Cache.php:16 +#, php-format +msgid "Cannot purge %s: no directory" +msgstr "Impossible de purger %s: le répertoire n'existe pas" + +#: application/FeedBuilder.php:146 +msgid "Direct link" +msgstr "Liens directs" + +#: application/FeedBuilder.php:148 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:242 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:245 +#: tmp/paper.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 +msgid "Permalink" +msgstr "Permalien" + +#: application/History.php:158 +msgid "History file isn't readable or writable" +msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" + +#: application/History.php:169 +msgid "Could not parse history file" +msgstr "Format incorrect pour le fichier d'historique" + +#: application/LinkDB.php:136 +msgid "You are not authorized to add a link." +msgstr "Vous n'êtes pas autorisé à ajouter un lien." + +#: application/LinkDB.php:139 +msgid "Internal Error: A link should always have an id and URL." +msgstr "Erreur interne : un lien devrait toujours avoir un ID et une URL." + +#: application/LinkDB.php:142 +msgid "You must specify an integer as a key." +msgstr "Vous devez utiliser un entier comme clé." + +#: application/LinkDB.php:145 +msgid "Array offset and link ID must be equal." +msgstr "La clé du tableau et l'ID du lien doivent être égaux." + +#: application/LinkDB.php:251 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14 +msgid "" +"The personal, minimalist, super-fast, database free, bookmarking service" +msgstr "" +"Le gestionnaire de marque-page personnel, minimaliste, et sans base de " +"données" + +#: application/LinkDB.php:253 +msgid "" +"Welcome to Shaarli! This is your first public bookmark. To edit or delete " +"me, you must first login.\n" +"\n" +"To learn how to use Shaarli, consult the link \"Documentation\" at the " +"bottom of this page.\n" +"\n" +"You use the community supported version of the original Shaarli project, by " +"Sebastien Sauvage." +msgstr "" +"Bienvenue sur Shaarli ! Ceci est votre premier marque-page public. Pour me " +"modifier ou me supprimer, vous devez d'abord vous connecter.\n" +"\n" +"Pour apprendre comment utiliser Shaarli, consultez le lien « Documentation » " +"en bas de page.\n" +"\n" +"Vous utilisez la version supportée par la communauté du projet original " +"Shaarli, de Sébastien Sauvage." + +#: application/LinkDB.php:267 +msgid "My secret stuff... - Pastebin.com" +msgstr "Mes trucs secrets... - Pastebin.com" + +#: application/LinkDB.php:269 +msgid "Shhhh! I'm a private link only YOU can see. You can delete me too." +msgstr "" +"Pssst ! Je suis un lien privé que VOUS êtes le seul à voir. Vous pouvez me " +"supprimer aussi." + +#: application/LinkFilter.php:376 +msgid "The link you are trying to reach does not exist or has been deleted." +msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé." + +#: application/NetscapeBookmarkUtils.php:35 +msgid "Invalid export selection:" +msgstr "Sélection d'export invalide :" + +#: application/NetscapeBookmarkUtils.php:80 +#, php-format +msgid "File %s (%d bytes) " +msgstr "Le fichier %s (%d octets) " + +#: application/NetscapeBookmarkUtils.php:82 +msgid "has an unknown file format. Nothing was imported." +msgstr "a un format inconnu. Rien n'a été importé." + +#: application/NetscapeBookmarkUtils.php:85 +#, php-format +msgid "" +"was successfully processed: %d links imported, %d links overwritten, %d " +"links skipped." +msgstr "" +"a été importé avec succès : %d liens importés, %d liens écrasés, %d liens " +"ignorés." + +#: application/PageBuilder.php:159 +msgid "The page you are trying to reach does not exist or has been deleted." +msgstr "La page que vous essayez de consulter n'existe pas ou a été supprimée." + +#: application/PageBuilder.php:161 +#, fuzzy +#| msgid " 404 Not Found" +msgid "404 Not Found" +msgstr "404 Introuvable" + +#: application/PluginManager.php:243 +#, php-format +msgid "Plugin \"%s\" files not found." +msgstr "Les fichiers de l'extension \"%s\" sont introuvables." + +#: application/Updater.php:76 +msgid "Couldn't retrieve Updater class methods." +msgstr "Impossible de récupérer les méthodes de la classe Updater." + +#: application/Updater.php:500 +msgid "An error occurred while running the update " +msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour " + +#: application/Updater.php:540 +msgid "Updates file path is not set, can't write updates." +msgstr "" +"Le chemin vers le fichier de mise à jour n'est pas défini, impossible " +"d'écrire les mises à jour." + +#: application/Updater.php:545 +msgid "Unable to write updates in " +msgstr "Impossible d'écrire les mises à jour dans " + +#: application/Utils.php:402 tests/UtilsTest.php:398 +msgid "Setting not set" +msgstr "Paramètre non défini" + +#: application/Utils.php:409 tests/UtilsTest.php:396 tests/UtilsTest.php:397 +msgid "Unlimited" +msgstr "Illimité" + +#: application/Utils.php:412 tests/UtilsTest.php:393 tests/UtilsTest.php:394 +#: tests/UtilsTest.php:408 +msgid "B" +msgstr "o" + +#: application/Utils.php:412 tests/UtilsTest.php:387 tests/UtilsTest.php:388 +#: tests/UtilsTest.php:395 +msgid "kiB" +msgstr "ko" + +#: application/Utils.php:412 tests/UtilsTest.php:389 tests/UtilsTest.php:390 +#: tests/UtilsTest.php:406 tests/UtilsTest.php:407 +msgid "MiB" +msgstr "Mo" + +#: application/Utils.php:412 tests/UtilsTest.php:391 tests/UtilsTest.php:392 +msgid "GiB" +msgstr "Go" + +#: application/config/ConfigJson.php:26 +#, php-format +msgid "" +"An error occurred while parsing JSON configuration file (%s): error code #%d" +msgstr "" +"Une erreur s'est produite lors de la lecture du fichier de configuration " +"JSON (%s) : code d'erreur #%d" + +#: application/config/ConfigJson.php:33 +msgid "" +"Please check your JSON syntax (without PHP comment tags) using a JSON lint " +"tool such as " +msgstr "" +"Merci de vérifier la syntaxe JSON (sans les balises de commentaires PHP) en " +"utilisant un validateur de JSON tel que " + +#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:121 +msgid "" +"Shaarli could not create the config file. Please make sure Shaarli has the " +"right to write in the folder is it installed in." +msgstr "" +"Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que " +"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé." + +#: application/config/ConfigManager.php:135 +msgid "Invalid setting key parameter. String expected, got: " +msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu :" + +#: application/config/exception/MissingFieldConfigException.php:21 +#, php-format +msgid "Configuration value is required for %s" +msgstr "Le paramètre %s est obligatoire" + +#: application/config/exception/PluginConfigOrderException.php:15 +msgid "An error occurred while trying to save plugins loading order." +msgstr "" +"Une erreur s'est produite lors de la sauvegarde de l'ordre des extensions." + +#: application/config/exception/UnauthorizedConfigException.php:16 +msgid "You are not authorized to alter config." +msgstr "Vous n'êtes pas autorisé à modifier la configuration." + +#: application/exceptions/IOException.php:19 +msgid "Error accessing" +msgstr "Une erreur s'est produite en accédant à" + +#: index.php:48 +msgid "" +"Error: missing Composer dependencies\n" +"\n" +"If you installed Shaarli through Git or using the development branch,\n" +"please refer to the installation documentation to install PHP dependencies " +"using Composer:\n" +msgstr "" +"Erreur : les dépendances Composer sont manquantes\n" +"\n" +"Si vous avez installé Shaarli avec Git ou depuis la branche de " +"développement\n" +"merci de consulter la documentation d'installation pour installer les " +"dépendances Composer :\n" +"\n" + +#: index.php:137 +msgid "Shared links on " +msgstr "Liens partagés sur " + +#: index.php:168 +msgid "Insufficient permissions:" +msgstr "Permissions insuffisantes :" + +#: index.php:415 +msgid "I said: NO. You are banned for the moment. Go away." +msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard." + +#: index.php:479 +msgid "Wrong login/password." +msgstr "Nom d'utilisateur ou mot de passe incorrects." + +#: index.php:1072 +msgid "You are not supposed to change a password on an Open Shaarli." +msgstr "" +"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert." + +#: index.php:1077 index.php:1118 index.php:1189 index.php:1243 index.php:1350 +msgid "Wrong token." +msgstr "Jeton invalide." + +#: index.php:1082 +msgid "The old password is not correct." +msgstr "L'ancien mot de passe est incorrect." + +#: index.php:1102 +msgid "Your password has been changed" +msgstr "Votre mot de passe a été modifié" + +#: index.php:1153 +msgid "Configuration was saved." +msgstr "La configuration a été sauvegardé." + +#: index.php:1206 +#, php-format +msgid "Tag was removed from %d links." +msgstr "Le tag a été supprimé de %d liens." + +#: index.php:1225 +#, php-format +msgid "Tag was renamed in %d links." +msgstr "Le tag a été renommé dans %d liens." + +#: index.php:1544 +#, php-format +msgid "" +"The file you are trying to upload is probably bigger than what this " +"webserver can accept (%s). Please upload in smaller chunks." +msgstr "" +"Le fichier que vous essayer d'envoyer est probablement plus lourd que ce que " +"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus " +"légères." + +#: index.php:1941 +#, php-format +msgid "" +"
Sessions do not seem to work correctly on your server.
Make sure the " +"variable \"session.save_path\" is set correctly in your PHP config, and that " +"you have write access to it.
It currently points to %s.
On some " +"browsers, accessing your server via a hostname like 'localhost' or any " +"custom hostname without a dot causes cookie storage to fail. We recommend " +"accessing your server via it's IP address or Fully Qualified Domain Name.
" +msgstr "" +"
Les sesssions ne semble pas fonctionner sur ce serveur.
Assurez vous " +"que la variable « session.save_path » est correctement définie dans votre " +"fichier de configuration PHP, et que vous y avez les droits d'écriture." +"
Ce paramètre pointe actuellement sur %s.
Sur certains navigateurs, " +"accéder à votre serveur depuis un nom d'hôte comme « localhost » ou autre " +"nom personnalisé sans point '.' entraine l'échec de la sauvegarde des " +"cookies. Nous vous recommandons d'accéder à votre serveur depuis son adresse " +"IP ou un Fully Qualified Domain Name.
" + +#: index.php:1951 +msgid "Click to try again." +msgstr "Cliquer ici pour réessayer." + +#: plugins/addlink_toolbar/addlink_toolbar.php:29 +msgid "URI" +msgstr "URI" + +#: plugins/addlink_toolbar/addlink_toolbar.php:33 +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "Add link" +msgstr "Shaare" + +#: plugins/addlink_toolbar/addlink_toolbar.php:50 +msgid "Adds the addlink input on the linklist page." +msgstr "Ajout le formulaire d'ajout de liens sur la page principale." + +#: plugins/archiveorg/archiveorg.php:23 +msgid "View on archive.org" +msgstr "Voir sur archive.org" + +#: plugins/archiveorg/archiveorg.php:36 +msgid "For each link, add an Archive.org icon." +msgstr "Pour chaque lien, ajoute une icône pour Archive.org." + +#: plugins/demo_plugin/demo_plugin.php:443 +msgid "" +"A demo plugin covering all use cases for template designers and plugin " +"developers." +msgstr "" +"Une extension de démonstration couvrant tous les cas d'utilisation pour les " +"designers et les développeurs." + +#: plugins/isso/isso.php:20 +msgid "" +"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin " +"administration page." +msgstr "" +"Erreur de l'extension Isso : Merci de définir le paramètre « ISSO_SERVER » " +"dans la page d'administration des extensions." + +#: plugins/isso/isso.php:63 +msgid "Let visitor comment your shaares on permalinks with Isso." +msgstr "" +"Permet aux visiteurs de commenter vos shaares sur les permaliens avec Isso." + +#: plugins/isso/isso.php:64 +msgid "Isso server URL (without 'http://')" +msgstr "URL du serveur Isso (sans 'http://')" + +#: plugins/markdown/markdown.php:150 +msgid "Description will be rendered with" +msgstr "La description sera générée avec" + +#: plugins/markdown/markdown.php:151 +msgid "Markdown syntax documentation" +msgstr "Documentation sur la syntaxe Markdown" + +#: plugins/markdown/markdown.php:152 +msgid "Markdown syntax" +msgstr "la syntaxe Markdown" + +#: plugins/markdown/markdown.php:311 +msgid "" +"Render shaare description with Markdown syntax.
Warning:\n" +"If your shaared descriptions contained HTML tags before enabling the " +"markdown plugin,\n" +"enabling it might break your page.\n" +"See the README." +msgstr "" +"Utilise la syntaxe Markdown pour la description des liens." +"
Attention :\n" +"Si vous aviez des descriptions contenant du HTML avant d'activer cette " +"extension,\n" +"l'activer pourrait déformer vos pages.\n" +"Voir le README." + +#: plugins/piwik/piwik.php:21 +msgid "" +"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " +"administration page." +msgstr "" +"Erreur de l'extension Piwik : Merci de définir les paramètres PIWIK_URL et " +"PIWIK_SITEID dans la page d'administration des extensions." + +#: plugins/piwik/piwik.php:70 +msgid "A plugin that adds Piwik tracking code to Shaarli pages." +msgstr "Ajoute le code de traçage de Piwik sur les pages de Shaarli." + +#: plugins/piwik/piwik.php:71 +msgid "Piwik URL" +msgstr "URL de Piwik" + +#: plugins/piwik/piwik.php:72 +msgid "Piwik site ID" +msgstr "Site ID de Piwik" + +#: plugins/playvideos/playvideos.php:22 +msgid "Video player" +msgstr "Lecteur vidéo" + +#: plugins/playvideos/playvideos.php:25 +msgid "Play Videos" +msgstr "Jouer les vidéos" + +#: plugins/playvideos/playvideos.php:56 +msgid "Add a button in the toolbar allowing to watch all videos." +msgstr "" +"Ajoute un bouton dans la barre de menu pour regarder toutes les vidéos." + +#: plugins/playvideos/youtube_playlist.js:214 +msgid "plugins/playvideos/jquery-1.11.2.min.js" +msgstr "" + +#: plugins/pubsubhubbub/pubsubhubbub.php:69 +#, php-format +msgid "Could not publish to PubSubHubbub: %s" +msgstr "Impossible de publier vers PubSubHubbub : %s" + +#: plugins/pubsubhubbub/pubsubhubbub.php:95 +#, php-format +msgid "Could not post to %s" +msgstr "Impossible de publier vers %s" + +#: plugins/pubsubhubbub/pubsubhubbub.php:99 +#, php-format +msgid "Bad response from the hub %s" +msgstr "Mauvaise réponse du hub %s" + +#: plugins/pubsubhubbub/pubsubhubbub.php:110 +msgid "Enable PubSubHubbub feed publishing." +msgstr "Active la publication de flux vers PubSubHubbub" + +#: plugins/qrcode/qrcode.php:69 plugins/wallabag/wallabag.php:68 +msgid "For each link, add a QRCode icon." +msgstr "Pour chaque liens, ajouter une icône de QRCode." + +#: plugins/wallabag/wallabag.php:21 +msgid "" +"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the " +"plugin administration page." +msgstr "" +"Erreur de l'extension Wallabag : Merci de définir le paramètre « " +"WALLABAG_URL » dans la page d'administration des extensions." + +#: plugins/wallabag/wallabag.php:47 +msgid "Save to wallabag" +msgstr "Sauvegarder dans Wallabag" + +#: plugins/wallabag/wallabag.php:69 +msgid "Wallabag API URL" +msgstr "URL de l'API Wallabag " + +#: plugins/wallabag/wallabag.php:70 +msgid "Wallabag API version (1 or 2)" +msgstr "Version de l'API Wallabag (1 ou 2)" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +msgid "Shaare a new link" +msgstr "Partager un nouveau lien" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "URL or leave empty to post a note" +msgstr "URL ou laisser vide pour créer une note" + +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "Change password" +msgstr "Modification du mot de passe" + +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Current password" +msgstr "Mot de passe actuel" + +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "New password" +msgstr "Nouveau mot de passe\t" + +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +msgid "Change" +msgstr "Changer" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +msgid "Manage tags" +msgstr "Gérer les tags" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 +msgid "Tag" +msgstr "Tag" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +msgid "New name" +msgstr "Nouveau nom" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 +msgid "Case sensitive" +msgstr "Sensible à la casse" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 +msgid "Rename" +msgstr "Renommer" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +#: tmp/editlink.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:60 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:288 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:313 +msgid "Delete" +msgstr "Supprimer" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +msgid "Configure" +msgstr "Configurer" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "title" +msgstr "titre" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +msgid "Home link" +msgstr "Lien vers l'accueil" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +msgid "Default value" +msgstr "Valeur par défaut" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +msgid "Theme" +msgstr "Thème" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 +msgid "Timezone" +msgstr "Fuseau horaire" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 +msgid "Continent" +msgstr "Continent" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 +msgid "City" +msgstr "Ville" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 +msgid "Redirector" +msgstr "Redirecteur" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135 +msgid "e. g." +msgstr "ex :" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135 +msgid "will mask the HTTP_REFERER" +msgstr "masque le HTTP_REFERER" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 +msgid "Disable session cookie hijacking protection" +msgstr "Désactiver la protection contre le détournement de cookies" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152 +msgid "Check this if you get disconnected or if your IP address changes often" +msgstr "" +"Cocher cette case si vous êtes souvent déconnecté ou si votre adresse IP " +"change souvent" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 +msgid "Private links by default" +msgstr "Liens privés par défaut" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170 +msgid "All new links are private by default" +msgstr "Tous les nouveaux liens sont privés par défaut" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185 +msgid "RSS direct links" +msgstr "Liens directs dans le flux RSS" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:186 +msgid "Check this to use direct URL instead of permalink in feeds" +msgstr "" +"Cocher cette case pour utiliser des liens directs au lieu des permaliens " +"dans le flux RSS" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:201 +msgid "Hide public links" +msgstr "Cacher les liens publics" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:202 +msgid "Do not show any links if the user is not logged in" +msgstr "N'afficher aucun lien sans être connecté" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:217 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:95 +msgid "Check updates" +msgstr "Vérifier les mises à jour" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:218 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97 +msgid "Notify me when a new release is ready" +msgstr "Me notifier lorsqu'une nouvelle version est disponible" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:233 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114 +msgid "Enable REST API" +msgstr "Activer l'API REST" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:234 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:115 +msgid "Allow third party software to use Shaarli such as mobile application" +msgstr "" +"Permets aux applications tierces d'utiliser Shaarli, par exemple les " +"applications mobiles" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:249 +msgid "API secret" +msgstr "Clé d'API secrète" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:260 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:192 +msgid "Save" +msgstr "Enregistrer" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +#: tmp/paper.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "The Daily Shaarli" +msgstr "Le Quotidien Shaarli" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 +#: tmp/paper.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 +msgid "1 RSS entry per day" +msgstr "1 entrée RSS par jour" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 +#: tmp/paper.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 +msgid "Previous day" +msgstr "Jour précédent" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +#: tmp/paper.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +msgid "All links of one day in a single page." +msgstr "Tous les liens d'un jour sur une page." + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 +#: tmp/paper.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 +msgid "Next day" +msgstr "Jour suivant" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26 +msgid "Shaare" +msgstr "Shaare" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 +msgid "URL" +msgstr "URL" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27 +msgid "Title" +msgstr "Titre" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124 +msgid "Description" +msgstr "Description" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 +msgid "Tags" +msgstr "Tags" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177 +msgid "Private" +msgstr "Privé" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Export Database" +msgstr "Exporter les données" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +msgid "Selection" +msgstr "Choisir" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 +msgid "All" +msgstr "Tous" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +msgid "Public" +msgstr "Publics" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52 +msgid "Prepend note permalinks with this Shaarli instance's URL" +msgstr "Préfixer les liens de notes avec l'URL de l'instance de Shaarli" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 +msgid "Useful to import bookmarks in a web browser" +msgstr "Utile pour importer les marques-pages dans un navigateur" + +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 +msgid "Export" +msgstr "Exporter" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Import Database" +msgstr "Importer des données" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +msgid "Maximum size allowed:" +msgstr "Taille maximum autorisée :" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "Visibility" +msgstr "Visibilité" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +msgid "Use values from the imported file, default to public" +msgstr "" +"Utiliser les valeurs présentes dans le fichier d'import, public par défaut" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +msgid "Import all bookmarks as private" +msgstr "Importer tous les liens comme privés" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +msgid "Import all bookmarks as public" +msgstr "Importer tous les liens comme publics" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57 +msgid "Overwrite existing bookmarks" +msgstr "Remplacer les liens existants" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +msgid "Duplicates based on URL" +msgstr "Les doublons s'appuient sur les URL" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 +msgid "Add default tags" +msgstr "Ajouter des tags par défaut" + +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 +msgid "Import" +msgstr "Importer" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 +msgid "Install Shaarli" +msgstr "Installation de Shaarli" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 +msgid "It looks like it's the first time you run Shaarli. Please configure it." +msgstr "" +"Il semblerait que ça soit la première fois que vous lancez Shaarli. Merci de " +"le configurer." + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:149 +msgid "Username" +msgstr "Nom d'utilisateur" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:150 +msgid "Password" +msgstr "Mot de passe" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80 +msgid "Shaarli title" +msgstr "Titre du Shaarli" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 +msgid "My links" +msgstr "Mes liens" + +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:127 +msgid "Install" +msgstr "Installer" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 +msgid "shaare" +msgid_plural "shaares" +msgstr[0] "shaare" +msgstr[1] "shaares" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 +msgid "private link" +msgid_plural "private links" +msgstr[0] "lien privé" +msgstr[1] "liens privés" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:119 +msgid "Search text" +msgstr "Recherche texte" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:131 +msgid "Filter by tag" +msgstr "Filtrer par tag" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118 +msgid "Nothing found." +msgstr "Aucun résultat." + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:126 +#, php-format +msgid "%s result" +msgid_plural "%s results" +msgstr[0] "%s résultat" +msgstr[1] "%s résultats" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130 +msgid "for" +msgstr "pour" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:137 +msgid "tagged" +msgstr "taggé" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:141 +msgid "Remove tag" +msgstr "Retirer le tag" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 +msgid "with status" +msgstr "avec le statut" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181 +msgid "Edit" +msgstr "Modifier" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182 +msgid "Fold" +msgstr "Replier" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:245 +msgid "Edited: " +msgstr "Modifié :" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:257 +msgid "permalink" +msgstr "permalien" + +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:7 +msgid "Filters" +msgstr "Filtres" + +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:12 +msgid "Filter private links" +msgstr "Filtrer par liens privés" + +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:63 +msgid "Links per page" +msgstr "Liens par page" + +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "" +"You have been banned after too many failed login attempts. Try again later." +msgstr "" +"Vous avez été banni après trop d'échec d'authentification. Merci de " +"réessayer plus tard." + +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:71 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:95 +msgid "Login" +msgstr "Connexion" + +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:153 +msgid "Remember me" +msgstr "Rester connecté" + +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14 +msgid "by the Shaarli community" +msgstr "par la communauté Shaarli" + +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 +msgid "Documentation" +msgstr "Documentation" + +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31 +msgid "Tools" +msgstr "Outils" + +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36 +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +#: tmp/tagcloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Tag cloud" +msgstr "Nuage de tags" + +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:39 +msgid "Picture wall" +msgstr "Mur d'images" + +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:42 +msgid "Daily" +msgstr "Quotidien" + +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:61 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:86 +msgid "RSS Feed" +msgstr "Flux RSS" + +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:66 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:102 +msgid "Logout" +msgstr "Déconnexion" + +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:81 +msgid "Search" +msgstr "Rechercher" + +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:171 +msgid "is available" +msgstr "est disponible" + +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:178 +msgid "Error" +msgstr "Erreur" + +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Picture Wall" +msgstr "Mur d'images" + +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "pics" +msgstr "images" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "You need to enable Javascript to change plugin loading order." +msgstr "" +"Vous devez activer Javascript pour pouvoir modifier l'ordre des extensions." + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 +msgid "Plugin administration" +msgstr "Administration des extensions" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "Enabled Plugins" +msgstr "Extensions activées" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155 +msgid "No plugin enabled." +msgstr "Aucune extension activée." + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73 +msgid "Disable" +msgstr "Désactiver" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:98 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 +msgid "Name" +msgstr "Nom" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 +msgid "Order" +msgstr "Ordre" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 +msgid "Disabled Plugins" +msgstr "Extensions désactivées" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 +msgid "No plugin disabled." +msgstr "Aucune extension désactivée." + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 +msgid "Enable" +msgstr "Activer" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 +msgid "More plugins available" +msgstr "Plus d'extensions disponibles" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136 +msgid "in the documentation" +msgstr "dans la documentation" + +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 +msgid "Plugin configuration" +msgstr "Configuration des extensions" + +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +#: tmp/tagcloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "tags" +msgstr "tags" + +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "Tag list" +msgstr "List des tags" + +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3 +msgid "Sort by:" +msgstr "Trier par :" + +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5 +msgid "Cloud" +msgstr "Nuage" + +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6 +msgid "Most used" +msgstr "Plus utilisés" + +#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7 +msgid "Alphabetical" +msgstr "Alphabétique" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +msgid "Settings" +msgstr "Paramètres" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "Change Shaarli settings: title, timezone, etc." +msgstr "Changer les paramètres de Shaarli : titre, fuseau horaire, etc." + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 +msgid "Configure your Shaarli" +msgstr "Conguration de Shaarli" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 +msgid "Enable, disable and configure plugins" +msgstr "Activer, désactiver et configurer les extensions" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +msgid "Change your password" +msgstr "Modification du mot de passe" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +msgid "Rename or delete a tag in all links" +msgstr "Rename or delete a tag in all links" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +msgid "" +"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " +"delicious...)" +msgstr "" +"Importer des marques pages au format Netscape HTML (comme exportés depuis " +"Firefox, Chrome, Opera, delicious...)" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +msgid "Import links" +msgstr "Importer des liens" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +msgid "" +"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " +"Opera, delicious...)" +msgstr "" +"Exporter les marques pages au format Netscape HTML (comme exportés depuis " +"Firefox, Chrome, Opera, delicious...)" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +msgid "Export database" +msgstr "Exporter les données" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71 +msgid "" +"Drag one of these button to your bookmarks toolbar or right-click it and " +"\"Bookmark This Link\"" +msgstr "" +"Glisser un de ces bouttons dans votre barre de favoris ou cliquer droit " +"dessus et « Ajouter aux favoris »" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 +msgid "then click on the bookmarklet in any page you want to share." +msgstr "" +"puis cliquer sur le marque page depuis un site que vous souhaitez partager." + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:95 +msgid "" +"Drag this link to your bookmarks toolbar or right-click it and Bookmark This " +"Link" +msgstr "" +"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " +"Ajouter aux favoris »" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 +msgid "then click ✚Shaare link button in any page you want to share" +msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 +msgid "Shaare link" +msgstr "Shaare" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 +msgid "" +"Then click ✚Add Note button anytime to start composing a private Note (text " +"post) to your Shaarli" +msgstr "" +"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 +msgid "Add Note" +msgstr "Ajouter une Note" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111 +msgid "" +"You need to browse your Shaarli over HTTPS to use this " +"functionality." +msgstr "" +"Vous devez utiliser Shaarli en HTTPS pour utiliser cette " +"fonctionalité." + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116 +msgid "Add to" +msgstr "Ajouter à" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:127 +msgid "3rd party" +msgstr "Applications tierces" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135 +msgid "Plugin" +msgstr "Extension" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136 +msgid "plugin" +msgstr "extension" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:157 +msgid "" +"Drag this link to your bookmarks toolbar, or right-click it and choose " +"Bookmark This Link" +msgstr "" +"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « " +"Ajouter aux favoris »" + +#~ msgid "Sessions do not seem to work correctly on your server." +#~ msgstr "Les sessions ne semblent " + +#~ msgid "Tag was renamed in " +#~ msgstr "Le tag a été renommé dans " + +#, fuzzy +#~| msgid "My links" +#~ msgid " links" +#~ msgstr "Mes liens" + +#, fuzzy +#~| msgid "" +#~| "Error: missing Composer configuration\n" +#~| "\n" +#~ msgid "Error: missing Composer configuration" +#~ msgstr "" +#~ "Erreur : la configuration Composer est manquante\n" +#~ "\n" + +#, fuzzy +#~| msgid "" +#~| "Shaarli could not create the config file. Please make sure Shaarli has " +#~| "the right to write in the folder is it installed in." +#~ msgid "" +#~ "Shaarli could not create the config file. \n" +#~ " Please make sure Shaarli has the right to write in the " +#~ "folder is it installed in." +#~ msgstr "" +#~ "Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier " +#~ "que Shaarli a les droits d'écriture dans le dossier dans lequel il est " +#~ "installé." + +#, fuzzy +#~| msgid "Plugin" +#~ msgid "Plugin \"" +#~ msgstr "Extension" + +#~ msgid "Your PHP version is obsolete!" +#~ msgstr "Votre version de PHP est obsolète !" + +#~ msgid " Shaarli requires at least PHP " +#~ msgstr "Shaarli nécessite au moins PHP" diff --git a/index.php b/index.php index 4068a828..98171d78 100644 --- a/index.php +++ b/index.php @@ -64,7 +64,6 @@ require_once 'application/FileUtils.php'; require_once 'application/History.php'; require_once 'application/HttpUtils.php'; -require_once 'application/Languages.php'; require_once 'application/LinkDB.php'; require_once 'application/LinkFilter.php'; require_once 'application/LinkUtils.php'; @@ -76,6 +75,7 @@ require_once 'application/PluginManager.php'; require_once 'application/Router.php'; require_once 'application/Updater.php'; +use \Shaarli\Languages; use \Shaarli\ThemeUtils; use \Shaarli\Config\ConfigManager; @@ -121,8 +121,16 @@ } $conf = new ConfigManager(); + +// Sniff browser language and set date format accordingly. +if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']); +} + +new Languages(setlocale(LC_MESSAGES, 0), $conf); + $conf->setEmpty('general.timezone', date_default_timezone_get()); -$conf->setEmpty('general.title', 'Shared links on '. escape(index_url($_SERVER))); +$conf->setEmpty('general.title', t('Shared links on '). escape(index_url($_SERVER))); RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory @@ -144,7 +152,7 @@ $errors = ApplicationUtils::checkResourcePermissions($conf); if ($errors != array()) { - $message = '

Insufficient permissions:

    '; + $message = '

    '. t('Insufficient permissions:') .'

      '; foreach ($errors as $error) { $message .= '
    • '.$error.'
    • '; @@ -163,11 +171,6 @@ // a token depending of deployment salt, user password, and the current ip define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt'))); -// Sniff browser language and set date format accordingly. -if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { - autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']); -} - /** * Checking session state (i.e. is the user still logged in) * @@ -376,7 +379,7 @@ function ban_canLogin($conf) // Process login form: Check if login/password is correct. if (isset($_POST['login'])) { - if (!ban_canLogin($conf)) die('I said: NO. You are banned for the moment. Go away.'); + if (!ban_canLogin($conf)) die(t('I said: NO. You are banned for the moment. Go away.')); if (isset($_POST['password']) && tokenOk($_POST['token']) && (check_auth($_POST['login'], $_POST['password'], $conf)) @@ -440,7 +443,8 @@ function ban_canLogin($conf) } } } - echo ''; // Redirect to login screen. + // Redirect to login screen. + echo ''; exit; } } @@ -1100,16 +1104,19 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) if ($targetPage == Router::$PAGE_CHANGEPASSWORD) { if ($conf->get('security.open_shaarli')) { - die('You are not supposed to change a password on an Open Shaarli.'); + die(t('You are not supposed to change a password on an Open Shaarli.')); } if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) { - if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away! + if (!tokenOk($_POST['token'])) die(t('Wrong token.')); // Go away! // Make sure old password is correct. $oldhash = sha1($_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt')); - if ($oldhash!= $conf->get('credentials.hash')) { echo ''; exit; } + if ($oldhash!= $conf->get('credentials.hash')) { + echo ''; + exit; + } // Save new password // Salt renders rainbow-tables attacks useless. $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); @@ -1127,7 +1134,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) echo ''; exit; } - echo ''; + echo ''; exit; } else // show the change password form. @@ -1143,7 +1150,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) if (!empty($_POST['title']) ) { if (!tokenOk($_POST['token'])) { - die('Wrong token.'); // Go away! + die(t('Wrong token.')); // Go away! } $tz = 'UTC'; if (!empty($_POST['continent']) && !empty($_POST['city']) @@ -1178,7 +1185,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) echo ''; exit; } - echo ''; + echo ''; exit; } else // Show the configuration form. @@ -1215,7 +1222,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) } if (!tokenOk($_POST['token'])) { - die('Wrong token.'); + die(t('Wrong token.')); } $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), escape($_POST['totag'])); @@ -1244,7 +1251,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) { // Go away! if (! tokenOk($_POST['token'])) { - die('Wrong token.'); + die(t('Wrong token.')); } // lf_id should only be present if the link exists. @@ -1344,7 +1351,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) if ($targetPage == Router::$PAGE_DELETELINK) { if (! tokenOk($_GET['token'])) { - die('Wrong token.'); + die(t('Wrong token.')); } $ids = trim($_GET['lf_linkdate']); @@ -1550,11 +1557,14 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) // Import bookmarks from an uploaded file if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) { // The file is too big or some form field may be missing. - echo ''; + $msg = sprintf( + t( + 'The file you are trying to upload is probably bigger than what this webserver can accept' + .' (%s). Please upload in smaller chunks.' + ), + get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')) + ); + echo ''; exit; } if (! tokenOk($_POST['token'])) { @@ -1962,12 +1972,20 @@ function install($conf) // (Because on some hosts, session.save_path may not be set correctly, // or we may not have write access to it.) if (isset($_GET['test_session']) && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) - { // Step 2: Check if data in session is correct. - echo '
      Sessions do not seem to work correctly on your server.
      '; - echo 'Make sure the variable session.save_path is set correctly in your php config, and that you have write access to it.
      '; - echo 'It currently points to '.session_save_path().'
      '; - echo 'Check that the hostname used to access Shaarli contains a dot. On some browsers, accessing your server via a hostname like \'localhost\' or any custom hostname without a dot causes cookie storage to fail. We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.
      '; - echo '
      Click to try again.
      '; + { + // Step 2: Check if data in session is correct. + $msg = t( + '
      Sessions do not seem to work correctly on your server.
      '. + 'Make sure the variable "session.save_path" is set correctly in your PHP config, '. + 'and that you have write access to it.
      '. + 'It currently points to %s.
      '. + 'On some browsers, accessing your server via a hostname like \'localhost\' '. + 'or any custom hostname without a dot causes cookie storage to fail. '. + 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.
      ' + ); + $msg = sprintf($msg, session_save_path()); + echo $msg; + echo '
      '. t('Click to try again.') .'
      '; die; } if (!isset($_SESSION['session_tested'])) diff --git a/plugins/TODO.md b/plugins/TODO.md deleted file mode 100644 index e3313d67..00000000 --- a/plugins/TODO.md +++ /dev/null @@ -1,28 +0,0 @@ -https://github.com/shaarli/Shaarli/issues/181 - Add Disqus or Isso comments box on a permalink page - - * http://posativ.org/isso/ - * install debian package https://packages.debian.org/sid/isso - * configure server http://posativ.org/isso/docs/configuration/server/ - * configure client http://posativ.org/isso/docs/configuration/client/ - * http://posativ.org/isso/docs/quickstart/ and add `` to includes.html template; then add `
      ` in the linklist template where you want the comments (in the linklist_plugins loop for example) - - -Problem: by default, Isso thread ID is guessed from the current url (only one thread per page). -if we want multiple threads on a single page (shaarli linklist), we must use : the `data-isso-id` client config, -with data-isso-id being the permalink of an item. - -`
      ` -`data-isso-id: Set a custom thread id, defaults to current URI.` - -Problem: feature is currently broken https://github.com/posativ/isso/issues/27 - -Another option, only display isso threads when current URL is a permalink (`\?(A-Z|a-z|0-9|-){7}`) (only show thread -when displaying only this link), and just display a "comments" button on each linklist item. Optionally show the comment -count on each item using the API (http://posativ.org/isso/docs/extras/api/#get-comment-count). API requests can be done -by raintpl `{function` or client-side with js. The former should be faster if isso and shaarli are on ther same server. - -Showing all full isso threads in the linklist would destroy layout - ------------------------------------------------------------ - -http://www.git-attitude.fr/2014/11/04/git-rerere/ for the merge diff --git a/plugins/addlink_toolbar/addlink_toolbar.php b/plugins/addlink_toolbar/addlink_toolbar.php index ddf50aaf..8c05a231 100644 --- a/plugins/addlink_toolbar/addlink_toolbar.php +++ b/plugins/addlink_toolbar/addlink_toolbar.php @@ -26,11 +26,11 @@ function hook_addlink_toolbar_render_header($data) array( 'type' => 'text', 'name' => 'post', - 'placeholder' => 'URI', + 'placeholder' => t('URI'), ), array( 'type' => 'submit', - 'value' => 'Add link', + 'value' => t('Add link'), 'class' => 'bigbutton', ), ), @@ -40,3 +40,12 @@ function hook_addlink_toolbar_render_header($data) return $data; } + +/** + * This function is never called, but contains translation calls for GNU gettext extraction. + */ +function addlink_toolbar_dummy_translation() +{ + // meta + t('Adds the addlink input on the linklist page.'); +} diff --git a/plugins/archiveorg/archiveorg.html b/plugins/archiveorg/archiveorg.html index 0781fe35..ad501f47 100644 --- a/plugins/archiveorg/archiveorg.html +++ b/plugins/archiveorg/archiveorg.html @@ -1 +1,5 @@ -archive.org + + + archive.org + + diff --git a/plugins/archiveorg/archiveorg.php b/plugins/archiveorg/archiveorg.php index 03d13d0e..cda35751 100644 --- a/plugins/archiveorg/archiveorg.php +++ b/plugins/archiveorg/archiveorg.php @@ -20,9 +20,18 @@ function hook_archiveorg_render_linklist($data) if($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) { continue; } - $archive = sprintf($archive_html, $value['url']); + $archive = sprintf($archive_html, $value['url'], t('View on archive.org')); $value['link_plugin'][] = $archive; } return $data; } + +/** + * This function is never called, but contains translation calls for GNU gettext extraction. + */ +function archiveorg_dummy_translation() +{ + // meta + t('For each link, add an Archive.org icon.'); +} diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php index 8fdbf663..3a90ae6a 100644 --- a/plugins/demo_plugin/demo_plugin.php +++ b/plugins/demo_plugin/demo_plugin.php @@ -433,3 +433,12 @@ function hook_demo_plugin_render_feed($data) } return $data; } + +/** + * This function is never called, but contains translation calls for GNU gettext extraction. + */ +function demo_dummy_translation() +{ + // meta + t('A demo plugin covering all use cases for template designers and plugin developers.'); +} diff --git a/plugins/isso/isso.php b/plugins/isso/isso.php index ce16645f..5bc1cce2 100644 --- a/plugins/isso/isso.php +++ b/plugins/isso/isso.php @@ -4,10 +4,11 @@ * Plugin Isso. */ +use Shaarli\Config\ConfigManager; + /** * Display an error everywhere if the plugin is enabled without configuration. * - * @param $data array List of links * @param $conf ConfigManager instance * * @return mixed - linklist data with Isso plugin. @@ -16,8 +17,8 @@ function isso_init($conf) { $issoUrl = $conf->get('plugins.ISSO_SERVER'); if (empty($issoUrl)) { - $error = 'Isso plugin error: '. - 'Please define the "ISSO_SERVER" setting in the plugin administration page.'; + $error = t('Isso plugin error: '. + 'Please define the "ISSO_SERVER" setting in the plugin administration page.'); return array($error); } } @@ -52,3 +53,13 @@ function hook_isso_render_linklist($data, $conf) return $data; } + +/** + * This function is never called, but contains translation calls for GNU gettext extraction. + */ +function isso_dummy_translation() +{ + // meta + t('Let visitor comment your shaares on permalinks with Isso.'); + t('Isso server URL (without \'http://\')'); +} diff --git a/plugins/markdown/help.html b/plugins/markdown/help.html index 9c4e5ae0..ded3d347 100644 --- a/plugins/markdown/help.html +++ b/plugins/markdown/help.html @@ -1,5 +1,5 @@
      - Description will be rendered with - - Markdown syntax. + %s + + %s.
      diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php index 772c56e8..1531549d 100644 --- a/plugins/markdown/markdown.php +++ b/plugins/markdown/markdown.php @@ -154,8 +154,13 @@ function hook_markdown_render_includes($data) function hook_markdown_render_editlink($data) { // Load help HTML into a string - $data['edit_link_plugin'][] = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html'); - + $txt = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html'); + $translations = [ + t('Description will be rendered with'), + t('Markdown syntax documentation'), + t('Markdown syntax'), + ]; + $data['edit_link_plugin'][] = vsprintf($txt, $translations); // Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion. if (! in_array(NO_MD_TAG, $data['tags'])) { $data['tags'][NO_MD_TAG] = 0; @@ -325,3 +330,15 @@ function process_markdown($description, $escape = true, $allowedProtocols = []) return $processedDescription; } + +/** + * This function is never called, but contains translation calls for GNU gettext extraction. + */ +function markdown_dummy_translation() +{ + // meta + t('Render shaare description with Markdown syntax.
      Warning: +If your shaared descriptions contained HTML tags before enabling the markdown plugin, +enabling it might break your page. +See the README.'); +} diff --git a/plugins/piwik/piwik.php b/plugins/piwik/piwik.php index 4a2b48a1..ca00c2be 100644 --- a/plugins/piwik/piwik.php +++ b/plugins/piwik/piwik.php @@ -18,8 +18,8 @@ function piwik_init($conf) $piwikUrl = $conf->get('plugins.PIWIK_URL'); $piwikSiteid = $conf->get('plugins.PIWIK_SITEID'); if (empty($piwikUrl) || empty($piwikSiteid)) { - $error = 'Piwik plugin error: ' . - 'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.'; + $error = t('Piwik plugin error: ' . + 'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.'); return array($error); } } @@ -60,3 +60,14 @@ function hook_piwik_render_footer($data, $conf) return $data; } + +/** + * This function is never called, but contains translation calls for GNU gettext extraction. + */ +function piwik_dummy_translation() +{ + // meta + t('A plugin that adds Piwik tracking code to Shaarli pages.'); + t('Piwik URL'); + t('Piwik site ID'); +} diff --git a/plugins/playvideos/playvideos.php b/plugins/playvideos/playvideos.php index 64484504..c6d6b0cc 100644 --- a/plugins/playvideos/playvideos.php +++ b/plugins/playvideos/playvideos.php @@ -19,10 +19,10 @@ function hook_playvideos_render_header($data) $playvideo = array( 'attr' => array( 'href' => '#', - 'title' => 'Video player', + 'title' => t('Video player'), 'id' => 'playvideos', ), - 'html' => '► Play Videos' + 'html' => '► '. t('Play Videos') ); $data['buttons_toolbar'][] = $playvideo; } @@ -46,3 +46,12 @@ function hook_playvideos_render_footer($data) return $data; } + +/** + * This function is never called, but contains translation calls for GNU gettext extraction. + */ +function playvideos_dummy_translation() +{ + // meta + t('Add a button in the toolbar allowing to watch all videos.'); +} diff --git a/plugins/pubsubhubbub/pubsubhubbub.php b/plugins/pubsubhubbub/pubsubhubbub.php index 03b6757b..184b588b 100644 --- a/plugins/pubsubhubbub/pubsubhubbub.php +++ b/plugins/pubsubhubbub/pubsubhubbub.php @@ -10,6 +10,7 @@ */ use pubsubhubbub\publisher\Publisher; +use Shaarli\Config\ConfigManager; /** * Plugin init function - set the hub to the default appspot one. @@ -65,7 +66,7 @@ function hook_pubsubhubbub_save_link($data, $conf) $p = new Publisher($conf->get('plugins.PUBSUBHUB_URL')); $p->publish_update($feeds, $httpPost); } catch (Exception $e) { - error_log('Could not publish to PubSubHubbub: ' . $e->getMessage()); + error_log(sprintf(t('Could not publish to PubSubHubbub: %s'), $e->getMessage())); } return $data; @@ -91,11 +92,20 @@ function nocurl_http_post($url, $postString) { $context = stream_context_create($params); $fp = @fopen($url, 'rb', false, $context); if (!$fp) { - throw new Exception('Could not post to '. $url); + throw new Exception(sprintf(t('Could not post to %s'), $url)); } $response = @stream_get_contents($fp); if ($response === false) { - throw new Exception('Bad response from the hub '. $url); + throw new Exception(sprintf(t('Bad response from the hub %s'), $url)); } return $response; } + +/** + * This function is never called, but contains translation calls for GNU gettext extraction. + */ +function pubsubhubbub_dummy_translation() +{ + // meta + t('Enable PubSubHubbub feed publishing.'); +} diff --git a/plugins/qrcode/qrcode.meta b/plugins/qrcode/qrcode.meta index cbf371ea..1812cd21 100644 --- a/plugins/qrcode/qrcode.meta +++ b/plugins/qrcode/qrcode.meta @@ -1 +1 @@ -description="For each link, add a QRCode icon ." +description="For each link, add a QRCode icon." diff --git a/plugins/qrcode/qrcode.php b/plugins/qrcode/qrcode.php index 8bc610d1..0f96a106 100644 --- a/plugins/qrcode/qrcode.php +++ b/plugins/qrcode/qrcode.php @@ -59,3 +59,12 @@ function hook_qrcode_render_includes($data) return $data; } + +/** + * This function is never called, but contains translation calls for GNU gettext extraction. + */ +function qrcode_dummy_translation() +{ + // meta + t('For each link, add a QRCode icon.'); +} diff --git a/plugins/wallabag/wallabag.html b/plugins/wallabag/wallabag.html index e861536d..4c57691d 100644 --- a/plugins/wallabag/wallabag.html +++ b/plugins/wallabag/wallabag.html @@ -1 +1,5 @@ -wallabag + + + wallabag + + diff --git a/plugins/wallabag/wallabag.php b/plugins/wallabag/wallabag.php index 641e4cc2..9dfd079e 100644 --- a/plugins/wallabag/wallabag.php +++ b/plugins/wallabag/wallabag.php @@ -5,6 +5,7 @@ */ require_once 'WallabagInstance.php'; +use Shaarli\Config\ConfigManager; /** * Init function, return an error if the server is not set. @@ -17,8 +18,8 @@ function wallabag_init($conf) { $wallabagUrl = $conf->get('plugins.WALLABAG_URL'); if (empty($wallabagUrl)) { - $error = 'Wallabag plugin error: '. - 'Please define the "WALLABAG_URL" setting in the plugin administration page.'; + $error = t('Wallabag plugin error: '. + 'Please define the "WALLABAG_URL" setting in the plugin administration page.'); return array($error); } } @@ -43,12 +44,14 @@ function hook_wallabag_render_linklist($data, $conf) $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html'); + $linkTitle = t('Save to wallabag'); foreach ($data['links'] as &$value) { $wallabag = sprintf( $wallabagHtml, $wallabagInstance->getWallabagUrl(), urlencode($value['url']), - PluginManager::$PLUGINS_PATH + PluginManager::$PLUGINS_PATH, + $linkTitle ); $value['link_plugin'][] = $wallabag; } @@ -56,3 +59,14 @@ function hook_wallabag_render_linklist($data, $conf) return $data; } +/** + * This function is never called, but contains translation calls for GNU gettext extraction. + */ +function wallabag_dummy_translation() +{ + // meta + t('For each link, add a QRCode icon.'); + t('Wallabag API URL'); + t('Wallabag API version (1 or 2)'); +} + diff --git a/tests/LanguagesTest.php b/tests/LanguagesTest.php index 79c136c8..46bfcd72 100644 --- a/tests/LanguagesTest.php +++ b/tests/LanguagesTest.php @@ -1,41 +1,201 @@ conf = new ConfigManager(self::$configFile); + } + /** * Test t() with a simple non identified value. */ - public function testTranslateSingleNotID() + public function testTranslateSingleNotIDGettext() { + $this->conf->set('translation.mode', 'gettext'); + new Languages('en', $this->conf); $text = 'abcdé 564 fgK'; $this->assertEquals($text, t($text)); } /** - * Test t() with a non identified plural form. + * Test t() with a simple identified value in gettext mode. */ - public function testTranslatePluralNotID() + public function testTranslateSingleIDGettext() { - $text = '%s sandwich'; - $nText = '%s sandwiches'; - $this->assertEquals('0 sandwich', t($text, $nText)); - $this->assertEquals('1 sandwich', t($text, $nText, 1)); - $this->assertEquals('2 sandwiches', t($text, $nText, 2)); + $this->conf->set('translation.mode', 'gettext'); + new Languages('en', $this->conf); + $text = 'permalink'; + $this->assertEquals($text, t($text)); } /** - * Test t() with a non identified invalid plural form. + * Test t() with a non identified plural form in gettext mode. */ - public function testTranslatePluralNotIDInvalid() + public function testTranslatePluralNotIDGettext() { + $this->conf->set('translation.mode', 'gettext'); + new Languages('en', $this->conf); $text = 'sandwich'; $nText = 'sandwiches'; + $this->assertEquals('sandwiches', t($text, $nText, 0)); $this->assertEquals('sandwich', t($text, $nText, 1)); $this->assertEquals('sandwiches', t($text, $nText, 2)); } + + /** + * Test t() with an identified plural form in gettext mode. + */ + public function testTranslatePluralIDGettext() + { + $this->conf->set('translation.mode', 'gettext'); + new Languages('en', $this->conf); + $text = 'shaare'; + $nText = 'shaares'; + // In english, zero is followed by plural form + $this->assertEquals('shaares', t($text, $nText, 0)); + $this->assertEquals('shaare', t($text, $nText, 1)); + $this->assertEquals('shaares', t($text, $nText, 2)); + } + + /** + * Test t() with a simple non identified value. + */ + public function testTranslateSingleNotIDPhp() + { + $this->conf->set('translation.mode', 'php'); + new Languages('en', $this->conf); + $text = 'abcdé 564 fgK'; + $this->assertEquals($text, t($text)); + } + + /** + * Test t() with a simple identified value in PHP mode. + */ + public function testTranslateSingleIDPhp() + { + $this->conf->set('translation.mode', 'php'); + new Languages('en', $this->conf); + $text = 'permalink'; + $this->assertEquals($text, t($text)); + } + + /** + * Test t() with a non identified plural form in PHP mode. + */ + public function testTranslatePluralNotIDPhp() + { + $this->conf->set('translation.mode', 'php'); + new Languages('en', $this->conf); + $text = 'sandwich'; + $nText = 'sandwiches'; + $this->assertEquals('sandwiches', t($text, $nText, 0)); + $this->assertEquals('sandwich', t($text, $nText, 1)); + $this->assertEquals('sandwiches', t($text, $nText, 2)); + } + + /** + * Test t() with an identified plural form in PHP mode. + */ + public function testTranslatePluralIDPhp() + { + $this->conf->set('translation.mode', 'php'); + new Languages('en', $this->conf); + $text = 'shaare'; + $nText = 'shaares'; + // In english, zero is followed by plural form + $this->assertEquals('shaares', t($text, $nText, 0)); + $this->assertEquals('shaare', t($text, $nText, 1)); + $this->assertEquals('shaares', t($text, $nText, 2)); + } + + /** + * Test t() with an invalid language set in the configuration in gettext mode. + */ + public function testTranslateWithInvalidConfLanguageGettext() + { + $this->conf->set('translation.mode', 'gettext'); + $this->conf->set('translation.language', 'nope'); + new Languages('fr', $this->conf); + $text = 'grumble'; + $this->assertEquals($text, t($text)); + } + + /** + * Test t() with an invalid language set in the configuration in PHP mode. + */ + public function testTranslateWithInvalidConfLanguagePhp() + { + $this->conf->set('translation.mode', 'php'); + $this->conf->set('translation.language', 'nope'); + new Languages('fr', $this->conf); + $text = 'grumble'; + $this->assertEquals($text, t($text)); + } + + /** + * Test t() with an invalid language set with auto language in gettext mode. + */ + public function testTranslateWithInvalidAutoLanguageGettext() + { + $this->conf->set('translation.mode', 'gettext'); + new Languages('nope', $this->conf); + $text = 'grumble'; + $this->assertEquals($text, t($text)); + } + + /** + * Test t() with an invalid language set with auto language in PHP mode. + */ + public function testTranslateWithInvalidAutoLanguagePhp() + { + $this->conf->set('translation.mode', 'php'); + new Languages('nope', $this->conf); + $text = 'grumble'; + $this->assertEquals($text, t($text)); + } + + /** + * Test t() with an extension language file in gettext mode + */ + public function testTranslationExtensionGettext() + { + $this->conf->set('translation.mode', 'gettext'); + $this->conf->set('translation.extensions.test', 'tests/utils/languages/'); + new Languages('en', $this->conf); + $this->assertEquals('car', t('car', 'car', 1, 'test')); + $this->assertEquals('Search', t('Search', 'Search', 1, 'test')); + } + + /** + * Test t() with an extension language file in PHP mode + */ + public function testTranslationExtensionPhp() + { + $this->conf->set('translation.mode', 'php'); + $this->conf->set('translation.extensions.test', 'tests/utils/languages/'); + new Languages('en', $this->conf); + $this->assertEquals('car', t('car', 'car', 1, 'test')); + $this->assertEquals('Search', t('Search', 'Search', 1, 'test')); + } } diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index 3d1aa653..840eaf21 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php @@ -384,18 +384,18 @@ public function testReturnBytes() */ public function testHumanBytes() { - $this->assertEquals('2kiB', human_bytes(2 * 1024)); - $this->assertEquals('2kiB', human_bytes(strval(2 * 1024))); - $this->assertEquals('2MiB', human_bytes(2 * (pow(1024, 2)))); - $this->assertEquals('2MiB', human_bytes(strval(2 * (pow(1024, 2))))); - $this->assertEquals('2GiB', human_bytes(2 * (pow(1024, 3)))); - $this->assertEquals('2GiB', human_bytes(strval(2 * (pow(1024, 3))))); - $this->assertEquals('374B', human_bytes(374)); - $this->assertEquals('374B', human_bytes('374')); - $this->assertEquals('232kiB', human_bytes(237481)); - $this->assertEquals('Unlimited', human_bytes('0')); - $this->assertEquals('Unlimited', human_bytes(0)); - $this->assertEquals('Setting not set', human_bytes('')); + $this->assertEquals('2'. t('kiB'), human_bytes(2 * 1024)); + $this->assertEquals('2'. t('kiB'), human_bytes(strval(2 * 1024))); + $this->assertEquals('2'. t('MiB'), human_bytes(2 * (pow(1024, 2)))); + $this->assertEquals('2'. t('MiB'), human_bytes(strval(2 * (pow(1024, 2))))); + $this->assertEquals('2'. t('GiB'), human_bytes(2 * (pow(1024, 3)))); + $this->assertEquals('2'. t('GiB'), human_bytes(strval(2 * (pow(1024, 3))))); + $this->assertEquals('374'. t('B'), human_bytes(374)); + $this->assertEquals('374'. t('B'), human_bytes('374')); + $this->assertEquals('232'. t('kiB'), human_bytes(237481)); + $this->assertEquals(t('Unlimited'), human_bytes('0')); + $this->assertEquals(t('Unlimited'), human_bytes(0)); + $this->assertEquals(t('Setting not set'), human_bytes('')); } /** @@ -403,9 +403,9 @@ public function testHumanBytes() */ public function testGetMaxUploadSize() { - $this->assertEquals('1MiB', get_max_upload_size(2097152, '1024k')); - $this->assertEquals('1MiB', get_max_upload_size('1m', '2m')); - $this->assertEquals('100B', get_max_upload_size(100, 100)); + $this->assertEquals('1'. t('MiB'), get_max_upload_size(2097152, '1024k')); + $this->assertEquals('1'. t('MiB'), get_max_upload_size('1m', '2m')); + $this->assertEquals('100'. t('B'), get_max_upload_size(100, 100)); } /** diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..d36d73cd --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,6 @@ +conf = new ConfigManager(self::$configFile); + $this->conf->set('translation.language', 'fr'); + } + + /** + * Reset the locale since gettext seems to mess with it, making it too long + */ + public static function tearDownAfterClass() + { + if (! empty(getenv('UT_LOCALE'))) { + setlocale(LC_ALL, getenv('UT_LOCALE')); + } + } + + /** + * Test t() with a simple non identified value. + */ + public function testTranslateSingleNotIDGettext() + { + $this->conf->set('translation.mode', 'gettext'); + new Languages('en', $this->conf); + $text = 'abcdé 564 fgK'; + $this->assertEquals($text, t($text)); + } + + /** + * Test t() with a simple identified value in gettext mode. + */ + public function testTranslateSingleIDGettext() + { + $this->conf->set('translation.mode', 'gettext'); + new Languages('en', $this->conf); + $text = 'permalink'; + $this->assertEquals('permalien', t($text)); + } + + /** + * Test t() with a non identified plural form in gettext mode. + */ + public function testTranslatePluralNotIDGettext() + { + $this->conf->set('translation.mode', 'gettext'); + new Languages('en', $this->conf); + $text = 'sandwich'; + $nText = 'sandwiches'; + // Not ID, so English fallback, and in english, plural 0 + $this->assertEquals('sandwiches', t($text, $nText, 0)); + $this->assertEquals('sandwich', t($text, $nText, 1)); + $this->assertEquals('sandwiches', t($text, $nText, 2)); + } + + /** + * Test t() with an identified plural form in gettext mode. + */ + public function testTranslatePluralIDGettext() + { + $this->conf->set('translation.mode', 'gettext'); + new Languages('en', $this->conf); + $text = 'shaare'; + $nText = 'shaares'; + $this->assertEquals('shaare', t($text, $nText, 0)); + $this->assertEquals('shaare', t($text, $nText, 1)); + $this->assertEquals('shaares', t($text, $nText, 2)); + } + + /** + * Test t() with a simple non identified value. + */ + public function testTranslateSingleNotIDPhp() + { + $this->conf->set('translation.mode', 'php'); + new Languages('en', $this->conf); + $text = 'abcdé 564 fgK'; + $this->assertEquals($text, t($text)); + } + + /** + * Test t() with a simple identified value in PHP mode. + */ + public function testTranslateSingleIDPhp() + { + $this->conf->set('translation.mode', 'php'); + new Languages('en', $this->conf); + $text = 'permalink'; + $this->assertEquals('permalien', t($text)); + } + + /** + * Test t() with a non identified plural form in PHP mode. + */ + public function testTranslatePluralNotIDPhp() + { + $this->conf->set('translation.mode', 'php'); + new Languages('en', $this->conf); + $text = 'sandwich'; + $nText = 'sandwiches'; + // Not ID, so English fallback, and in english, plural 0 + $this->assertEquals('sandwiches', t($text, $nText, 0)); + $this->assertEquals('sandwich', t($text, $nText, 1)); + $this->assertEquals('sandwiches', t($text, $nText, 2)); + } + + /** + * Test t() with an identified plural form in PHP mode. + */ + public function testTranslatePluralIDPhp() + { + $this->conf->set('translation.mode', 'php'); + new Languages('en', $this->conf); + $text = 'shaare'; + $nText = 'shaares'; + // In english, zero is followed by plural form + $this->assertEquals('shaare', t($text, $nText, 0)); + $this->assertEquals('shaare', t($text, $nText, 1)); + $this->assertEquals('shaares', t($text, $nText, 2)); + } + + /** + * Test t() with an extension language file in gettext mode + */ + public function testTranslationExtensionGettext() + { + $this->conf->set('translation.mode', 'gettext'); + $this->conf->set('translation.extensions.test', 'tests/utils/languages/'); + new Languages('en', $this->conf); + $this->assertEquals('voiture', t('car', 'car', 1, 'test')); + $this->assertEquals('Fouille', t('Search', 'Search', 1, 'test')); + } + + /** + * Test t() with an extension language file in PHP mode + */ + public function testTranslationExtensionPhp() + { + $this->conf->set('translation.mode', 'php'); + $this->conf->set('translation.extensions.test', 'tests/utils/languages/'); + new Languages('en', $this->conf); + $this->assertEquals('voiture', t('car', 'car', 1, 'test')); + $this->assertEquals('Fouille', t('Search', 'Search', 1, 'test')); + } +} diff --git a/tests/utils/languages/fr/LC_MESSAGES/test.mo b/tests/utils/languages/fr/LC_MESSAGES/test.mo new file mode 100644 index 0000000000000000000000000000000000000000..416c783105300fb5ecdfdb2636b060c338b65e55 GIT binary patch literal 456 zcmZvX&rSj{5XS5OWRIRb^r$h#?g~mu@Pe!%LL{&nZ#pgmTeG&L+a>rQzKchn#Ak69 zj9i@bm-#yR+J5~$-2GrwJH!!DC-#UwQPnfCPXJMQGU9fNv7Gt=@kzSsU({)>s`73B zYBol2X~t4;Z0PJOre5?W;sITutx>$Y^k^!{Jr+I~-X)^r5Ijx9HF#7!lHsM04G~Em zo~uUvR7O&gQH*e*tCRou>MFcg`}$CLkvK3#4&FM&gFcv92{RN4!kgmb48Z03Z>;## zJ;kG7&>M6&F~gb+I@VBDy6t^Vu{j>PPCaR z-h9#Y$Gmiqi`criLUA)+ZHso);%nJHHvEc*C#~R7+@L|X4nDLv(Vj;&SqlRe+fwH_ LvW3;YbjW@H<$QQq literal 0 HcmV?d00001 diff --git a/tests/utils/languages/fr/LC_MESSAGES/test.po b/tests/utils/languages/fr/LC_MESSAGES/test.po new file mode 100644 index 00000000..89a4fd9b --- /dev/null +++ b/tests/utils/languages/fr/LC_MESSAGES/test.po @@ -0,0 +1,19 @@ +msgid "" +msgstr "" +"Project-Id-Version: Extension test\n" +"POT-Creation-Date: 2017-05-20 13:54+0200\n" +"PO-Revision-Date: 2017-05-20 14:16+0200\n" +"Last-Translator: \n" +"Language-Team: Shaarli\n" +"Language: fr_FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 2.0.1\n" + +msgid "car" +msgstr "voiture" + +msgid "Search" +msgstr "Fouille" diff --git a/tpl/default/import.html b/tpl/default/import.html index 1f040685..000a50ac 100644 --- a/tpl/default/import.html +++ b/tpl/default/import.html @@ -18,7 +18,7 @@

      {"Import Database"|t}

      -


      Maximum size allowed: {$maxfilesizeHuman}

      +


      {'Maximum size allowed:'|t} {$maxfilesizeHuman}

      @@ -31,15 +31,15 @@

      {"Import Database"|t}

      - Use values from the imported file, default to public + {'Use values from the imported file, default to public'|t}
      - Import all bookmarks as private + {'Import all bookmarks as private'|t}
      - Import all bookmarks as public + {'Import all bookmarks as public'|t}
      diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html index 685821e3..5dab8e9a 100644 --- a/tpl/default/linklist.html +++ b/tpl/default/linklist.html @@ -86,7 +86,7 @@
      - {function="t('%s result', '%s results', $result_count)"} + {function="sprintf(t('%s result', '%s results', $result_count), $result_count)"} {if="!empty($search_term)"} {'for'|t} {$search_term} {/if} @@ -117,6 +117,16 @@
      + {ignore}Set translation here, for performances{/ignore} + {$strPrivate=t('Private')} + {$strEdit=t('Edit')} + {$strDelete=t('Delete')} + {$strFold=t('Fold')} + {$strEdited=t('Edited: ')} + {$strPermalink=t('Permalink')} + {$strPermalinkLc=t('permalink')} + {$strAddTag=t('Add tag')} + {ignore}End of translations{/ignore} {loop="links"}
      +
      +
      +
      + +
      +
      +
      +
      + +
      +
      +
      diff --git a/tpl/default/install.html b/tpl/default/install.html index 164d453b..6199b33d 100644 --- a/tpl/default/install.html +++ b/tpl/default/install.html @@ -65,6 +65,27 @@

      {'Install Shaarli'|t}

      +
      +
      +
      + +
      +
      +
      +
      + +
      +
      +
      +
      From 6a65bc579810e3688a63a7c3b0e720dc0f5456b0 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 19 Aug 2017 10:53:19 +0200 Subject: [PATCH 16/36] Translations : Working demo example of translation extension --- application/Utils.php | 2 +- inc/languages/fr/LC_MESSAGES/shaarli.po | 70 +++++++++--------- plugins/demo_plugin/demo_plugin.php | 28 ++++++- .../languages/fr/LC_MESSAGES/demo.mo | Bin 0 -> 652 bytes .../languages/fr/LC_MESSAGES/demo.po | 21 ++++++ 5 files changed, 86 insertions(+), 35 deletions(-) create mode 100644 plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mo create mode 100644 plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po diff --git a/application/Utils.php b/application/Utils.php index 27eaafc5..2f38a8de 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -480,7 +480,7 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false) * @param int $nb The number of items for plural forms. * @param string $domain The domain where the translation is stored (default: shaarli). * - * @return String Text translated. + * @return string Text translated. */ function t($text, $nText = '', $nb = 1, $domain = 'shaarli') { return dn__($domain, $text, $nText, $nb); diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po index cb9161db..6b2de950 100644 --- a/inc/languages/fr/LC_MESSAGES/shaarli.po +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po @@ -1,15 +1,15 @@ msgid "" msgstr "" "Project-Id-Version: Shaarli\n" -"POT-Creation-Date: 2017-09-01 19:21+0200\n" -"PO-Revision-Date: 2017-09-01 19:21+0200\n" +"POT-Creation-Date: 2017-10-22 13:13+0200\n" +"PO-Revision-Date: 2017-10-22 13:14+0200\n" "Last-Translator: \n" "Language-Team: Shaarli\n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 2.0.3\n" +"X-Generator: Poedit 2.0.4\n" "X-Poedit-Basepath: ../../../..\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Poedit-SourceCharset: UTF-8\n" @@ -27,19 +27,19 @@ msgstr "" "peut donc pas fonctionner. Votre version de PHP a des failles de sécurités " "connues et devrait être mise à jour au plus tôt." -#: application/ApplicationUtils.php:182 application/ApplicationUtils.php:194 +#: application/ApplicationUtils.php:183 application/ApplicationUtils.php:195 msgid "directory is not readable" msgstr "le répertoire n'est pas accessible en lecture" -#: application/ApplicationUtils.php:197 +#: application/ApplicationUtils.php:198 msgid "directory is not writable" msgstr "le répertoire n'est pas accessible en écriture" -#: application/ApplicationUtils.php:215 +#: application/ApplicationUtils.php:216 msgid "file is not readable" msgstr "le fichier n'est pas accessible en lecture" -#: application/ApplicationUtils.php:218 +#: application/ApplicationUtils.php:219 msgid "file is not writable" msgstr "le fichier n'est pas accessible en écriture" @@ -58,11 +58,11 @@ msgstr "Liens directs" msgid "Permalink" msgstr "Permalien" -#: application/History.php:158 +#: application/History.php:174 msgid "History file isn't readable or writable" msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" -#: application/History.php:169 +#: application/History.php:185 msgid "Could not parse history file" msgstr "Format incorrect pour le fichier d'historique" @@ -135,7 +135,7 @@ msgstr "" "Pssst ! Je suis un lien privé que VOUS êtes le seul à voir. Vous pouvez me " "supprimer aussi." -#: application/LinkFilter.php:415 +#: application/LinkFilter.php:452 msgid "The link you are trying to reach does not exist or has been deleted." msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé." @@ -143,29 +143,29 @@ msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé. msgid "Invalid export selection:" msgstr "Sélection d'export invalide :" -#: application/NetscapeBookmarkUtils.php:80 +#: application/NetscapeBookmarkUtils.php:81 #, php-format msgid "File %s (%d bytes) " msgstr "Le fichier %s (%d octets) " -#: application/NetscapeBookmarkUtils.php:82 +#: application/NetscapeBookmarkUtils.php:83 msgid "has an unknown file format. Nothing was imported." msgstr "a un format inconnu. Rien n'a été importé." -#: application/NetscapeBookmarkUtils.php:85 +#: application/NetscapeBookmarkUtils.php:86 #, php-format msgid "" -"was successfully processed: %d links imported, %d links overwritten, %d " -"links skipped." +"was successfully processed in %d seconds: %d links imported, %d links " +"overwritten, %d links skipped." msgstr "" -"a été importé avec succès : %d liens importés, %d liens écrasés, %d liens " -"ignorés." +"a été importé avec succès en %d secondes : %d liens importés, %d liens " +"écrasés, %d liens ignorés." -#: application/PageBuilder.php:160 +#: application/PageBuilder.php:165 msgid "The page you are trying to reach does not exist or has been deleted." msgstr "La page que vous essayez de consulter n'existe pas ou a été supprimée." -#: application/PageBuilder.php:162 +#: application/PageBuilder.php:167 msgid "404 Not Found" msgstr "404 Introuvable" @@ -249,58 +249,62 @@ msgstr "Vous n'êtes pas autorisé à modifier la configuration." msgid "Error accessing" msgstr "Une erreur s'est produite en accédant à" -#: index.php:134 +#: index.php:133 msgid "Shared links on " msgstr "Liens partagés sur " -#: index.php:156 +#: index.php:155 msgid "Insufficient permissions:" msgstr "Permissions insuffisantes :" -#: index.php:383 +#: index.php:382 msgid "I said: NO. You are banned for the moment. Go away." msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard." -#: index.php:448 +#: index.php:447 msgid "Wrong login/password." msgstr "Nom d'utilisateur ou mot de passe incorrects." -#: index.php:1091 +#: index.php:1107 msgid "You are not supposed to change a password on an Open Shaarli." msgstr "" "Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert." -#: index.php:1096 index.php:1137 index.php:1213 index.php:1243 index.php:1343 +#: index.php:1112 index.php:1153 index.php:1229 index.php:1259 index.php:1359 msgid "Wrong token." msgstr "Jeton invalide." -#: index.php:1101 +#: index.php:1117 msgid "The old password is not correct." msgstr "L'ancien mot de passe est incorrect." -#: index.php:1121 +#: index.php:1137 msgid "Your password has been changed" msgstr "Votre mot de passe a été modifié" -#: index.php:1174 +#: index.php:1190 msgid "Configuration was saved." msgstr "La configuration a été sauvegardé." -#: index.php:1225 +#: index.php:1241 #, php-format msgid "The tag was removed from %d link." msgid_plural "The tag was removed from %d links." msgstr[0] "Le tag a été supprimé de %d lien." msgstr[1] "Le tag a été supprimé de %d liens." -#: index.php:1226 +#: index.php:1242 #, php-format msgid "The tag was renamed in %d link." msgid_plural "The tag was renamed in %d links." msgstr[0] "Le tag a été renommé dans %d lien." msgstr[1] "Le tag a été renommé dans %d liens." -#: index.php:1551 +#: index.php:1458 +msgid "Note: " +msgstr "Note : " + +#: index.php:1567 #, php-format msgid "" "The file you are trying to upload is probably bigger than what this " @@ -310,7 +314,7 @@ msgstr "" "le serveur web peut accepter (%s). Merci de l'envoyer en parties plus " "légères." -#: index.php:1967 +#: index.php:1983 #, php-format msgid "" "
      Sessions do not seem to work correctly on your server.
      Make sure the " @@ -329,7 +333,7 @@ msgstr "" "cookies. Nous vous recommandons d'accéder à votre serveur depuis son adresse " "IP ou un Fully Qualified Domain Name.
      " -#: index.php:1977 +#: index.php:1993 msgid "Click to try again." msgstr "Cliquer ici pour réessayer." diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php index 3a90ae6a..b80a2b6d 100644 --- a/plugins/demo_plugin/demo_plugin.php +++ b/plugins/demo_plugin/demo_plugin.php @@ -14,6 +14,26 @@ * and check user status with _LOGGEDIN_. */ +use Shaarli\Config\ConfigManager; + +/** + * In the footer hook, there is a working example of a translation extension for Shaarli. + * + * The extension must be attached to a new translation domain (i.e. NOT 'shaarli'). + * Use case: any custom theme or non official plugin can use the translation system. + * + * See the documentation for more information. + */ +const EXT_TRANSLATION_DOMAIN = 'demo'; + +/* + * This is not necessary, but it's easier if you don't want Poedit to mix up your translations. + */ +function demo_plugin_t($text, $nText = '', $nb = 1) +{ + return t($text, $nText, $nb, EXT_TRANSLATION_DOMAIN); +} + /** * Initialization function. * It will be called when the plugin is loaded. @@ -27,6 +47,12 @@ function demo_plugin_init($conf) { $conf->get('toto', 'nope'); + if (! $conf->exists('translation.extensions.demo')) { + // Custom translation with the domain 'demo' + $conf->set('translation.extensions.demo', 'plugins/demo_plugin/languages/'); + $conf->write(true); + } + $errors[] = 'This a demo init error.'; return $errors; } @@ -160,7 +186,7 @@ function hook_demo_plugin_render_includes($data) function hook_demo_plugin_render_footer($data) { // footer text - $data['text'][] = 'Shaarli is now enhanced by the awesome demo_plugin.'; + $data['text'][] = '
      '. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.'); // Free elements at the end of the page. $data['endofpage'][] = '' . diff --git a/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mo b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mo new file mode 100644 index 0000000000000000000000000000000000000000..0f80f6ed54884a3b4c2e9567662c86f68d555b17 GIT binary patch literal 652 zcmZuv!EO^V5M2;l_Q;vT96{7u?*eLY2#2OEib%zlq=;KJ-JN8OcI{w$8}b2s0w2-u z$baxH%x2X@LX7-mkNrG;e)gYdPku)nj~UMyM~tV8_lz!b#%snEWBVwIesO=wnD7Qp z_tEX&#jHYfwFd3M*fo%;&=gd{?FK?cfNS!$Atdo6%GLw>t;tSpBuEKwKsmcaDZ;C#M zxkKY?4Av=#r)ZWfE=kM@8?}?SpHr}K@#W<5{GcvPWIVPekl{L;6$=XVGaRpK)Mf}p zh0aqLUC%GX;K1w7TaXjyrm&?pO9g{l9pZ1|BN=kNkla$K17wLzGG6IZaf+T+7%Bxx za=dGCUbhZwabaEK6&QAkQ$NavooPHAy)f_r(wD 1);\n" +"X-Poedit-KeywordsList: ;demo_plugin_t:1,2;demo_plugin_t\n" +"X-Poedit-SourceCharset: UTF-8\n" +"X-Poedit-SearchPath-0: .\n" + +#: demo_plugin.php:173 +msgid "Shaarli is now enhanced by the awesome demo_plugin." +msgstr "Shaarli est maintenant amélioré avec le fantastique demo_plugin." From 1a47014f99d2f7aae00d37e62e0364d4eaa1ce29 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 1 Sep 2017 18:47:32 +0200 Subject: [PATCH 17/36] Translation documentation --- doc/md/Download-and-Installation.md | 4 +- doc/md/Server-requirements.md | 1 + doc/md/Shaarli-configuration.md | 21 ++++ doc/md/Translations.md | 152 ++++++++++++++++++++++++++++ doc/md/Upgrade-and-migration.md | 21 +++- doc/md/images/install-shaarli.png | Bin 0 -> 44376 bytes doc/md/images/poedit-1.jpg | Bin 0 -> 72956 bytes mkdocs.yml | 1 + 8 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 doc/md/Translations.md create mode 100644 doc/md/images/install-shaarli.png create mode 100644 doc/md/images/poedit-1.jpg diff --git a/doc/md/Download-and-Installation.md b/doc/md/Download-and-Installation.md index e5e929ef..59a1b7da 100644 --- a/doc/md/Download-and-Installation.md +++ b/doc/md/Download-and-Installation.md @@ -36,6 +36,7 @@ In most cases, download Shaarli from the [releases](https://github.com/shaarli/S $ mkdir -p /path/to/shaarli && cd /path/to/shaarli/ $ git clone -b v0.9 https://github.com/shaarli/Shaarli.git . $ composer install --no-dev --prefer-dist +$ make translate ``` ## Stable version @@ -83,13 +84,14 @@ $ git clone https://github.com/shaarli/Shaarli.git -b master /path/to/shaarli/ # install/update third-party dependencies $ cd /path/to/shaarli $ composer install --no-dev --prefer-dist +$ make translate ``` ## Finish Installation Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser. -![install screenshot](http://i.imgur.com/wuMpDSN.png) +![install screenshot](images/install-shaarli.png) Setup your Shaarli installation, and it's ready to use! diff --git a/doc/md/Server-requirements.md b/doc/md/Server-requirements.md index 707af762..400b85a9 100644 --- a/doc/md/Server-requirements.md +++ b/doc/md/Server-requirements.md @@ -39,3 +39,4 @@ Extension | Required? | Usage [`php-gd`](http://php.net/manual/en/book.image.php) | optional | thumbnail resizing [`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`) [`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way +[`php-gettext`](http://php.net/manual/en/book.gettext.php) | optional | Use the translation system in gettext mode (faster) diff --git a/doc/md/Shaarli-configuration.md b/doc/md/Shaarli-configuration.md index 99b25ba7..920c7e27 100644 --- a/doc/md/Shaarli-configuration.md +++ b/doc/md/Shaarli-configuration.md @@ -81,6 +81,20 @@ _These settings should not be edited_ - **page_cache**: Shaarli's internal cache directory. - **ban_file**: Banned IP file path. +### Translation + +- **language**: translation language (also see [Translations](Translations)) + - **auto** (default): The translation language is chosen from the browser locale. + It means that the language can be different for 2 different visitors depending on their locale. + - **en**: Use the English translation. + - **fr**: Use the French translation. +- **mode**: + - **auto** or **php** (default): Use the PHP implementation of gettext (slower) + - **gettext**: Use PHP builtin gettext extension + (faster, but requires `php-gettext` to be installed and to reload the web server on update) +- **extension**: Translation extensions for custom themes or plugins. +Must be an associative array: `translation domain => translation path`. + ### Updates - **check_updates**: Enable or disable update check to the git repository. @@ -211,6 +225,13 @@ _These settings should not be edited_ "plugins": { "WALLABAG_URL": "http://demo.wallabag.org", "WALLABAG_VERSION": "1" + }, + "translation": { + "language": "fr", + "mode": "php", + "extensions": { + "demo": "plugins/demo_plugin/languages/" + } } } ?> ``` diff --git a/doc/md/Translations.md b/doc/md/Translations.md new file mode 100644 index 00000000..54a36655 --- /dev/null +++ b/doc/md/Translations.md @@ -0,0 +1,152 @@ +## Translations + +Shaarli supports [gettext](https://www.gnu.org/software/gettext/manual/gettext.html) translations +since `>= v0.9.2`. + +Note that only the `default` theme supports translations. + +### Contributing + +We encourage the community to contribute to Shaarli's translation either by improving existing +translations or submitting a new language. + +Contributing to the translation does not require development skill. + +Please submit a pull request with the `.po` file updated/created. Note that the compiled file (`.mo`) +is not stored on the repository, and is generated during the release process. + +### How to + +First, install [Poedit](https://poedit.net/) tool. + +Poedit will extract strings to translate from the PHP source code. + +**Important**: due to the usage of a template engine, it's important to generate PHP cache files to extract +every translatable string. + +You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended) +or visit every template page in your browser to generate cache files, while logged in. + +Here is a list : + +``` +http:/// +http:///?nonope +http:///?do=addlink +http:///?do=changepasswd +http:///?do=changetag +http:///?do=configure +http:///?do=tools +http:///?do=daily +http:///?post +http:///?do=export +http:///?do=import +http:///?do=login +http:///?do=picwall +http:///?do=pluginadmin +http:///?do=tagcloud +http:///?do=taglist +``` + +#### Improve existing translation + +In Poedit, click on "Edit a Translation", and from Shaarli's directory open +`inc/languages//LC_MESSAGES/shaarli.po`. + +The existing list of translatable strings should have been loaded, then click on the "Update" button. + +You can start editing the translation. + +![poedit-screenshot](images/poedit-1.jpg) + +Save when you're done, then you can submit a pull request containing the updated `shaarli.po`. + +#### Add a new language + +Open Poedit and select "Create New Translation", then from Shaarli's directory open +`inc/languages//LC_MESSAGES/shaarli.po`. + +Then select the language you want to create. + +Click on `File > Save as...`, and save your file in `/inc/language//LC_MESSAGES/shaarli.po`. +`` here should be the language code respecting the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-2) +format in lowercase (e.g. `de` for German). + +Then click on the "Update" button, and you can start to translate every available string. + +Save when you're done, then you can submit a pull request containing the new `shaarli.po`. + +### Extend Shaarli's translation + +If you're writing a custom theme, or a non official plugin, you might want to use the translation system, +but you won't be able to able to override Shaarli's translation. + +However, you can add your own translation domain which extends the main translation list. + +> Note that you can find a live example of translation extension in the `demo_plugin`. + +First, create your translation files tree directory: + +``` +/languages//LC_MESSAGES/ +``` + +Your `.po` files must be named like your domain. E.g. if your translation domain is `my_theme`, then your file will be +`my_theme.po`. + +Users have to register your extension in their configuration with the parameter +`translation.extensions.: `. + +Example: + +```php +if (! $conf->exists('translation.extensions.my_theme')) { + $conf->set('translation.extensions.my_theme', '/languages/'); + $conf->write(true); +} +``` + +> Note that the page needs to be reloaded after the registration. + +It is then recommended to create a custom translation function which will call the `t()` function with your domain. +For example : + +```php +function my_theme_t($text, $nText = '', $nb = 1) +{ + return t($text, $nText, $nb, 'my_theme'); // the last parameter is your translation domain. +} +``` + +All strings which can be translated should be processed through your function: + +```php +my_theme_t('Comment'); +my_theme_t('Comment', 'Comments', 2); +``` + +Or in templates: + +```php +{'Comment'|my_theme_t} +{function="my_theme_t('Comment', 'Comments', 2)"} +``` + +> Note than in template, you need to visit your page at least once to generate a cache file. + +When you're done, open Poedit and load translation strings from sources: + + 1. `File > New` + 2. Choose your language + 3. Save your `PO` file in `/languages//LC_MESSAGES/my_theme.po`. + 4. Go to `Catalog > Properties...` + 5. Fill the `Translation Properties` tab + 6. Add your source path in the `Sources Paths` tab + 7. In the `Sources Keywords` tab uncheck "Also use default keywords" and add the following lines: + +``` +my_theme_t +my_theme_t:1,2 +``` + +Click on the "Update" button and you're free to start your translations! diff --git a/doc/md/Upgrade-and-migration.md b/doc/md/Upgrade-and-migration.md index 7033cd41..1dc07339 100644 --- a/doc/md/Upgrade-and-migration.md +++ b/doc/md/Upgrade-and-migration.md @@ -39,7 +39,10 @@ We recommend that you use the latest release tarball with the `-full` suffix. It Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory! -After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli-configuration) for more details). +If you use translations in gettext mode - meaning you manually changed the default mode -, +reload your web server. + +After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli configuration) for more details). ## Upgrading with Git @@ -72,6 +75,14 @@ Updating dependencies Downloading: 100% ``` +Shaarli >= `v0.9.2` supports translations: + +```bash +$ make translate +``` + +If you use translations in gettext mode, reload your web server. + ### Migrating and upgrading from Sebsauvage's repository If you have installed Shaarli from [Sebsauvage's original Git repository](https://github.com/sebsauvage/Shaarli), you can use [Git remotes](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) to update your working copy. @@ -151,6 +162,14 @@ Updating dependencies Downloading: 100% ``` +Shaarli >= `v0.9.2` supports translations: + +```bash +$ make translate +``` + +If you use translations in gettext mode, reload your web server. + Optionally, you can delete information related to the legacy version: ```bash diff --git a/doc/md/images/install-shaarli.png b/doc/md/images/install-shaarli.png new file mode 100644 index 0000000000000000000000000000000000000000..7ae338165b549359dbc65a26d22aa726137ab129 GIT binary patch literal 44376 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 bvLF@n@W{KR`wv^mWw45Jr(~0k>)!rfw7$ZU literal 0 HcmV?d00001 diff --git a/doc/md/images/poedit-1.jpg b/doc/md/images/poedit-1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..673ae6d6797d2bd431fa401acc7d0f8a23fb41c1 GIT binary patch literal 72956 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

      ILXCQptN9gTgmfG+aUhu6@lJN}OsKV+O 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 jg Date: Sun, 22 Oct 2017 14:08:05 +0200 Subject: [PATCH 18/36] Documentation: add reverse proxy examples for Docker images Closes https://github.com/shaarli/Shaarli/issues/888 Signed-off-by: VirtualTam --- doc/md/docker/reverse-proxy-configuration.md | 116 ++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/doc/md/docker/reverse-proxy-configuration.md b/doc/md/docker/reverse-proxy-configuration.md index 91ffecff..6066140e 100644 --- a/doc/md/docker/reverse-proxy-configuration.md +++ b/doc/md/docker/reverse-proxy-configuration.md @@ -1,6 +1,120 @@ +## Foreword + +This guide assumes that: + +- Shaarli runs in a Docker container +- The host's `10080` port is mapped to the container's `80` port +- Shaarli's Fully Qualified Domain Name (FQDN) is `shaarli.domain.tld` +- HTTP traffic is redirected to HTTPS + +## Apache + +- [Apache 2.4 documentation](https://httpd.apache.org/docs/2.4/) + - [mod_proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html) + - [Reverse Proxy Request Headers](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers) + +The following HTTP headers are set by using the `ProxyPass` directive: + +- `X-Forwarded-For` +- `X-Forwarded-Host` +- `X-Forwarded-Server` + +```apache + + ServerName shaarli.domain.tld + Redirect permanent / https://shaarli.domain.tld + + + + ServerName shaarli.domain.tld + + SSLEngine on + SSLCertificateFile /path/to/cert + SSLCertificateKeyFile /path/to/certkey + + LogLevel warn + ErrorLog /var/log/apache2/shaarli-error.log + CustomLog /var/log/apache2/shaarli-access.log combined + + RequestHeader set X-Forwarded-Proto "https" + + ProxyPass / http://127.0.0.1:10080/ + ProxyPassReverse / http://127.0.0.1:10080/ + +``` -TODO, see https://github.com/shaarli/Shaarli/issues/888 ## HAProxy +- [HAProxy documentation](https://cbonte.github.io/haproxy-dconv/) + +```conf +global + [...] + +defaults + [...] + +frontend http-in + bind :80 + redirect scheme https code 301 if !{ ssl_fc } + + bind :443 ssl crt /path/to/cert.pem + + default_backend shaarli + + +backend shaarli + mode http + option http-server-close + option forwardfor + reqadd X-Forwarded-Proto: https + + server shaarli1 127.0.0.1:10080 +``` + + ## Nginx + +- [Nginx documentation](https://nginx.org/en/docs/) + +```nginx +http { + [...] + + index index.html index.php; + + root /home/john/web; + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + server { + listen 80; + server_name shaarli.domain.tld; + return 301 https://shaarli.domain.tld$request_uri; + } + + server { + listen 443 ssl http2; + server_name shaarli.domain.tld; + + ssl_certificate /path/to/cert + ssl_certificate_key /path/to/certkey + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + + proxy_pass http://localhost:10080/; + proxy_set_header Host $host; + proxy_connect_timeout 30s; + proxy_read_timeout 120s; + + access_log /var/log/nginx/shaarli.access.log; + error_log /var/log/nginx/shaarli.error.log; + } + } +} +``` From ebd650c06c67a67da2a0d099f625b6a7ec62ab2b Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Sun, 22 Oct 2017 18:44:46 +0200 Subject: [PATCH 19/36] Refactor session token management Relates to https://github.com/shaarli/Shaarli/issues/324 Added: - `SessionManager` class to group session-related features - unit tests Changed: - `getToken()` -> `SessionManager->generateToken()` - `tokenOk()` -> `SessionManager->checkToken()` - inject a `$token` parameter to `PageBuilder`'s constructor Signed-off-by: VirtualTam --- application/PageBuilder.php | 6 ++- application/SessionManager.php | 53 +++++++++++++++++++++++++ index.php | 71 ++++++++++++--------------------- tests/SessionManagerTest.php | 72 ++++++++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 49 deletions(-) create mode 100644 application/SessionManager.php create mode 100644 tests/SessionManagerTest.php diff --git a/application/PageBuilder.php b/application/PageBuilder.php index af290671..468f144b 100644 --- a/application/PageBuilder.php +++ b/application/PageBuilder.php @@ -32,12 +32,14 @@ class PageBuilder * * @param ConfigManager $conf Configuration Manager instance (reference). * @param LinkDB $linkDB instance. + * @param string $token Session token */ - public function __construct(&$conf, $linkDB = null) + public function __construct(&$conf, $linkDB = null, $token = null) { $this->tpl = false; $this->conf = $conf; $this->linkDB = $linkDB; + $this->token = $token; } /** @@ -92,7 +94,7 @@ private function initialize() $this->tpl->assign('showatom', $this->conf->get('feed.show_atom', true)); $this->tpl->assign('feed_type', $this->conf->get('feed.show_atom', true) !== false ? 'atom' : 'rss'); $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false)); - $this->tpl->assign('token', getToken($this->conf)); + $this->tpl->assign('token', $this->token); if ($this->linkDB !== null) { $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); diff --git a/application/SessionManager.php b/application/SessionManager.php new file mode 100644 index 00000000..2083df42 --- /dev/null +++ b/application/SessionManager.php @@ -0,0 +1,53 @@ +session = &$session; + $this->conf = &$conf; + } + + /** + * Generates a session token + * + * @return string token + */ + public function generateToken() + { + $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt')); + $this->session['tokens'][$token] = 1; + return $token; + } + + /** + * Checks the validity of a session token, and destroys it afterwards + * + * @param string $token The token to check + * + * @return bool true if the token is valid, else false + */ + public function checkToken($token) + { + if (! isset($this->session['tokens'][$token])) { + // the token is wrong, or has already been used + return false; + } + + // destroy the token to prevent future use + unset($this->session['tokens'][$token]); + return true; + } +} diff --git a/index.php b/index.php index 1dc81843..9e698628 100644 --- a/index.php +++ b/index.php @@ -78,6 +78,7 @@ use \Shaarli\Languages; use \Shaarli\ThemeUtils; use \Shaarli\Config\ConfigManager; +use \Shaarli\SessionManager; // Ensure the PHP version is supported try { @@ -121,6 +122,7 @@ } $conf = new ConfigManager(); +$sessionManager = new SessionManager($_SESSION, $conf); // Sniff browser language and set date format accordingly. if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { @@ -165,7 +167,7 @@ } // Display the installation form if no existing config is found - install($conf); + install($conf, $sessionManager); } // a token depending of deployment salt, user password, and the current ip @@ -381,7 +383,7 @@ function ban_canLogin($conf) { if (!ban_canLogin($conf)) die(t('I said: NO. You are banned for the moment. Go away.')); if (isset($_POST['password']) - && tokenOk($_POST['token']) + && $sessionManager->checkToken($_POST['token']) && (check_auth($_POST['login'], $_POST['password'], $conf)) ) { // Login/password is OK. ban_loginOk($conf); @@ -454,32 +456,6 @@ function ban_canLogin($conf) // Token should be used in any form which acts on data (create,update,delete,import...). if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session. -/** - * Returns a token. - * - * @param ConfigManager $conf Configuration Manager instance. - * - * @return string token. - */ -function getToken($conf) -{ - $rnd = sha1(uniqid('', true) .'_'. mt_rand() . $conf->get('credentials.salt')); // We generate a random string. - $_SESSION['tokens'][$rnd]=1; // Store it on the server side. - return $rnd; -} - -// Tells if a token is OK. Using this function will destroy the token. -// true=token is OK. -function tokenOk($token) -{ - if (isset($_SESSION['tokens'][$token])) - { - unset($_SESSION['tokens'][$token]); // Token is used: destroy it. - return true; // Token is OK. - } - return false; // Wrong token, or already used. -} - /** * Daily RSS feed: 1 RSS entry per day giving all the links on that day. * Gives the last 7 days (which have links). @@ -687,12 +663,13 @@ function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) { /** * Render HTML page (according to URL parameters and user rights) * - * @param ConfigManager $conf Configuration Manager instance. - * @param PluginManager $pluginManager Plugin Manager instance, - * @param LinkDB $LINKSDB - * @param History $history instance + * @param ConfigManager $conf Configuration Manager instance. + * @param PluginManager $pluginManager Plugin Manager instance, + * @param LinkDB $LINKSDB + * @param History $history instance + * @param SessionManager $sessionManager SessionManager instance */ -function renderPage($conf, $pluginManager, $LINKSDB, $history) +function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager) { $updater = new Updater( read_updates_file($conf->get('resource.updates')), @@ -713,7 +690,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) die($e->getMessage()); } - $PAGE = new PageBuilder($conf, $LINKSDB); + $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken()); $PAGE->assign('linkcount', count($LINKSDB)); $PAGE->assign('privateLinkcount', count_private($LINKSDB)); $PAGE->assign('plugin_errors', $pluginManager->getErrors()); @@ -1109,13 +1086,13 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) { - if (!tokenOk($_POST['token'])) die(t('Wrong token.')); // Go away! + if (!$sessionManager->checkToken($_POST['token'])) die(t('Wrong token.')); // Go away! // Make sure old password is correct. $oldhash = sha1($_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt')); if ($oldhash!= $conf->get('credentials.hash')) { echo ''; - exit; + exit; } // Save new password // Salt renders rainbow-tables attacks useless. @@ -1149,7 +1126,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) { if (!empty($_POST['title']) ) { - if (!tokenOk($_POST['token'])) { + if (!$sessionManager->checkToken($_POST['token'])) { die(t('Wrong token.')); // Go away! } $tz = 'UTC'; @@ -1225,7 +1202,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) exit; } - if (!tokenOk($_POST['token'])) { + if (!$sessionManager->checkToken($_POST['token'])) { die(t('Wrong token.')); } @@ -1255,7 +1232,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) if (isset($_POST['save_edit'])) { // Go away! - if (! tokenOk($_POST['token'])) { + if (! $sessionManager->checkToken($_POST['token'])) { die(t('Wrong token.')); } @@ -1355,7 +1332,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) // -------- User clicked the "Delete" button when editing a link: Delete link from database. if ($targetPage == Router::$PAGE_DELETELINK) { - if (! tokenOk($_GET['token'])) { + if (! $sessionManager->checkToken($_GET['token'])) { die(t('Wrong token.')); } @@ -1572,7 +1549,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) echo ''; exit; } - if (! tokenOk($_POST['token'])) { + if (! $sessionManager->checkToken($_POST['token'])) { die('Wrong token.'); } $status = NetscapeBookmarkUtils::import( @@ -1639,7 +1616,7 @@ function($a, $b) { return $a['order'] - $b['order']; } // Get a fresh token if ($targetPage == Router::$GET_TOKEN) { header('Content-Type:text/plain'); - echo getToken($conf); + echo $sessionManager->generateToken($conf); exit; } @@ -1965,10 +1942,10 @@ function lazyThumbnail($conf, $url,$href=false) * Installation * This function should NEVER be called if the file data/config.php exists. * - * @param ConfigManager $conf Configuration Manager instance. + * @param ConfigManager $conf Configuration Manager instance. + * @param SessionManager $sessionManager SessionManager instance */ -function install($conf) -{ +function install($conf, $sessionManager) { // On free.fr host, make sure the /sessions directory exists, otherwise login will not work. if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705); @@ -2051,7 +2028,7 @@ function install($conf) exit; } - $PAGE = new PageBuilder($conf); + $PAGE = new PageBuilder($conf, null, $sessionManager->generateToken()); list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get()); $PAGE->assign('continents', $continents); $PAGE->assign('cities', $cities); @@ -2328,7 +2305,7 @@ function resizeImage($filepath) if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) { // We use UTF-8 for proper international characters handling. header('Content-Type: text/html; charset=utf-8'); - renderPage($conf, $pluginManager, $linkDb, $history); + renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager); } else { $app->respond($response); } diff --git a/tests/SessionManagerTest.php b/tests/SessionManagerTest.php new file mode 100644 index 00000000..3a270303 --- /dev/null +++ b/tests/SessionManagerTest.php @@ -0,0 +1,72 @@ +generateToken(); + + $this->assertEquals(1, $session['tokens'][$token]); + $this->assertEquals(40, strlen($token)); + } + + /** + * Generate and check a session token + */ + public function testGenerateAndCheckToken() + { + $session = []; + $conf = new FakeConfigManager(); + $sessionManager = new SessionManager($session, $conf); + + $token = $sessionManager->generateToken(); + + // ensure a token has been generated + $this->assertEquals(1, $session['tokens'][$token]); + $this->assertEquals(40, strlen($token)); + + // check and destroy the token + $this->assertTrue($sessionManager->checkToken($token)); + $this->assertFalse(isset($session['tokens'][$token])); + + // ensure the token has been destroyed + $this->assertFalse($sessionManager->checkToken($token)); + } + + /** + * Check an invalid session token + */ + public function testCheckInvalidToken() + { + $session = []; + $conf = new FakeConfigManager(); + $sessionManager = new SessionManager($session, $conf); + + $this->assertFalse($sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b')); + } +} From fd7d84616d53486c3a276a42da869390e1d7f5eb Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Sun, 22 Oct 2017 19:54:44 +0200 Subject: [PATCH 20/36] Move session ID check to SessionManager Relates to https://github.com/shaarli/Shaarli/issues/324 Changed: - `is_session_id_valid()` -> `SessionManager::checkId()` - update tests Signed-off-by: VirtualTam --- application/SessionManager.php | 30 +++++++++++++++ application/Utils.php | 30 --------------- index.php | 2 +- tests/SessionManagerTest.php | 67 +++++++++++++++++++++++++++++++++- tests/UtilsTest.php | 58 ----------------------------- 5 files changed, 97 insertions(+), 90 deletions(-) diff --git a/application/SessionManager.php b/application/SessionManager.php index 2083df42..3aa4ddfc 100644 --- a/application/SessionManager.php +++ b/application/SessionManager.php @@ -50,4 +50,34 @@ public function checkToken($token) unset($this->session['tokens'][$token]); return true; } + + /** + * Validate session ID to prevent Full Path Disclosure. + * + * See #298. + * The session ID's format depends on the hash algorithm set in PHP settings + * + * @param string $sessionId Session ID + * + * @return true if valid, false otherwise. + * + * @see http://php.net/manual/en/function.hash-algos.php + * @see http://php.net/manual/en/session.configuration.php + */ + public static function checkId($sessionId) + { + if (empty($sessionId)) { + return false; + } + + if (!$sessionId) { + return false; + } + + if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) { + return false; + } + + return true; + } } diff --git a/application/Utils.php b/application/Utils.php index 2f38a8de..97b12fcf 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -181,36 +181,6 @@ function generateLocation($referer, $host, $loopTerms = array()) return $finalReferer; } -/** - * Validate session ID to prevent Full Path Disclosure. - * - * See #298. - * The session ID's format depends on the hash algorithm set in PHP settings - * - * @param string $sessionId Session ID - * - * @return true if valid, false otherwise. - * - * @see http://php.net/manual/en/function.hash-algos.php - * @see http://php.net/manual/en/session.configuration.php - */ -function is_session_id_valid($sessionId) -{ - if (empty($sessionId)) { - return false; - } - - if (!$sessionId) { - return false; - } - - if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) { - return false; - } - - return true; -} - /** * Sniff browser language to set the locale automatically. * Note that is may not work on your server if the corresponding locale is not installed. diff --git a/index.php b/index.php index 9e698628..e1516d37 100644 --- a/index.php +++ b/index.php @@ -116,7 +116,7 @@ } // Regenerate session ID if invalid or not defined in cookie. -if (isset($_COOKIE['shaarli']) && !is_session_id_valid($_COOKIE['shaarli'])) { +if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) { session_regenerate_id(true); $_COOKIE['shaarli'] = session_id(); } diff --git a/tests/SessionManagerTest.php b/tests/SessionManagerTest.php index 3a270303..9fa60dc5 100644 --- a/tests/SessionManagerTest.php +++ b/tests/SessionManagerTest.php @@ -1,8 +1,12 @@ assertFalse($sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b')); } + + /** + * Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES! + * + * This tests extensively covers all hash algorithms / bit representations + */ + public function testIsAnyHashSessionIdValid() + { + foreach (self::$sidHashes as $algo => $bpcs) { + foreach ($bpcs as $bpc => $hash) { + $this->assertTrue(SessionManager::checkId($hash)); + } + } + } + + /** + * Test checkId with a valid ID - SHA-1 hashes + */ + public function testIsSha1SessionIdValid() + { + $this->assertTrue(SessionManager::checkId(sha1('shaarli'))); + } + + /** + * Test checkId with a valid ID - SHA-256 hashes + */ + public function testIsSha256SessionIdValid() + { + $this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli'))); + } + + /** + * Test checkId with a valid ID - SHA-512 hashes + */ + public function testIsSha512SessionIdValid() + { + $this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli'))); + } + + /** + * Test checkId with invalid IDs. + */ + public function testIsSessionIdInvalid() + { + $this->assertFalse(SessionManager::checkId('')); + $this->assertFalse(SessionManager::checkId([])); + $this->assertFalse( + SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=') + ); + } } diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index 840eaf21..6cd37a7a 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php @@ -5,10 +5,6 @@ require_once 'application/Utils.php'; require_once 'application/Languages.php'; -require_once 'tests/utils/ReferenceSessionIdHashes.php'; - -// Initialize reference data before PHPUnit starts a session -ReferenceSessionIdHashes::genAllHashes(); /** @@ -16,9 +12,6 @@ */ class UtilsTest extends PHPUnit_Framework_TestCase { - // Session ID hashes - protected static $sidHashes = null; - // Log file protected static $testLogFile = 'tests.log'; @@ -30,13 +23,11 @@ class UtilsTest extends PHPUnit_Framework_TestCase */ protected static $defaultTimeZone; - /** * Assign reference data */ 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'); @@ -221,56 +212,7 @@ public function testGenerateLocationOut() { $this->assertEquals('?', generateLocation($ref, 'localhost')); } - /** - * Test is_session_id_valid with a valid ID - TEST ALL THE HASHES! - * - * This tests extensively covers all hash algorithms / bit representations - */ - public function testIsAnyHashSessionIdValid() - { - foreach (self::$sidHashes as $algo => $bpcs) { - foreach ($bpcs as $bpc => $hash) { - $this->assertTrue(is_session_id_valid($hash)); - } - } - } - /** - * Test is_session_id_valid with a valid ID - SHA-1 hashes - */ - public function testIsSha1SessionIdValid() - { - $this->assertTrue(is_session_id_valid(sha1('shaarli'))); - } - - /** - * Test is_session_id_valid with a valid ID - SHA-256 hashes - */ - public function testIsSha256SessionIdValid() - { - $this->assertTrue(is_session_id_valid(hash('sha256', 'shaarli'))); - } - - /** - * Test is_session_id_valid with a valid ID - SHA-512 hashes - */ - public function testIsSha512SessionIdValid() - { - $this->assertTrue(is_session_id_valid(hash('sha512', 'shaarli'))); - } - - /** - * Test is_session_id_valid with invalid IDs. - */ - public function testIsSessionIdInvalid() - { - $this->assertFalse(is_session_id_valid('')); - $this->assertFalse(is_session_id_valid(array())); - $this->assertFalse( - is_session_id_valid('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=') - ); - } - /** * Test generateSecretApi. */ From fc2beb8c6aa4d423b55ba95809941f2eba6fea2a Mon Sep 17 00:00:00 2001 From: nodiscc Date: Mon, 23 Oct 2017 01:06:11 +0200 Subject: [PATCH 21/36] Changelog: link to CVE-2017-15215, give attribution --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 120c5d22..33feac20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Security -- Vulnerability introduced in v0.9.1 fixed. +- Fixed reflected XSS vulnerability introduced in v0.9.1, discovered by @chb9 ([CVE-2017-15215](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15215)). ## [v0.9.1](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) - 2017-08-23 From ae7c954b1279981cc23c9f67d88f55bfecc4d828 Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Tue, 24 Oct 2017 22:01:02 +0200 Subject: [PATCH 22/36] Improve SessionManager tests Signed-off-by: VirtualTam --- tests/SessionManagerTest.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/SessionManagerTest.php b/tests/SessionManagerTest.php index 9fa60dc5..a92c3ccc 100644 --- a/tests/SessionManagerTest.php +++ b/tests/SessionManagerTest.php @@ -50,6 +50,29 @@ public function testGenerateToken() $this->assertEquals(40, strlen($token)); } + /** + * Check a session token + */ + public function testCheckToken() + { + $token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'; + $session = [ + 'tokens' => [ + $token => 1, + ], + ]; + $conf = new FakeConfigManager(); + $sessionManager = new SessionManager($session, $conf); + + + // check and destroy the token + $this->assertTrue($sessionManager->checkToken($token)); + $this->assertFalse(isset($session['tokens'][$token])); + + // ensure the token has been destroyed + $this->assertFalse($sessionManager->checkToken($token)); + } + /** * Generate and check a session token */ From 94c1756562df22382ccd88fd202371a386c1801b Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 30 Sep 2017 14:14:40 +0200 Subject: [PATCH 23/36] Theme improvements: move thumbnails to the right and reduce margins overall * Reduce multiple margins (markdown, space between block, etc.) * Move thumbnails to the right in the same line as the title * Move edit button as floating to the left * Move fold/collapse and checkbox buttons as floating to the right * Add a bunch of HTML ID in the linklist template Relates to #877 --- tpl/default/css/shaarli.css | 102 ++++++++++++++++++++++++++---------- tpl/default/js/shaarli.js | 2 +- tpl/default/linklist.html | 69 ++++++++++++++---------- 3 files changed, 114 insertions(+), 59 deletions(-) diff --git a/tpl/default/css/shaarli.css b/tpl/default/css/shaarli.css index ba589723..9065f887 100644 --- a/tpl/default/css/shaarli.css +++ b/tpl/default/css/shaarli.css @@ -433,7 +433,7 @@ body, .pure-g [class*="pure-u"] { * 64em -> lg */ .linklist-filters { - margin: 10px 0; + margin: 5px 0; color: #252525; font-size: 0.9em; } @@ -454,7 +454,7 @@ body, .pure-g [class*="pure-u"] { } .linklist-pages { - margin: 10px 0; + margin: 5px 0; color: #252525; text-align: center; } @@ -469,7 +469,7 @@ body, .pure-g [class*="pure-u"] { } .linksperpage { - margin: 10px 0; + margin: 5px 0; text-align: right; color: #252525; font-size: 0.9em; @@ -506,9 +506,29 @@ body, .pure-g [class*="pure-u"] { * CONTENT - LINKLIST ITEMS */ .linklist-item { - margin: 0 0 15px 0; + margin: 0 0 10px 0; background: #f5f5f5; - box-shadow: 2px 2px 0.5em #797979; + box-shadow: 1px 1px 3px #797979; +} + +.linklist-item-buttons { + background: transparent; + position: relative; + width: 23px; + z-index: 99; +} + +.linklist-item-buttons-right { + float: right; + margin-right: -25px; +} + +.linklist-item-buttons * { + display: block; + float: left; + width:100%; + margin: auto; + text-align: center; } .linklist-item-title, .linklist-item-title h2 { @@ -526,7 +546,7 @@ body, .pure-g [class*="pure-u"] { line-height: 30px; } -.linklist-item-title a { +.linklist-item-title h2 a { font-size: 0.7em; color: #252525; text-decoration: none; @@ -538,11 +558,11 @@ body, .pure-g [class*="pure-u"] { color: #1b926c; } -.linklist-item-title a:visited .linklist-link { +.linklist-item-title h2 a:visited .linklist-link { color: #2a4c41; } -.linklist-item-title a:hover, .linklist-item-title .linklist-link:hover{ +.linklist-item-title h2 a:hover, .linklist-item-title .linklist-link:hover{ color: #252525; } @@ -554,8 +574,9 @@ body, .pure-g [class*="pure-u"] { color: #F89406; } -.linklist-item-title .fold-button { +.fold-button { display: none; + color: #252525; } .linklist-item-editbuttons { @@ -585,24 +606,12 @@ body, .pure-g [class*="pure-u"] { .linklist-item-description { position: relative; - padding: 10px; + padding: 0 10px; word-wrap: break-word; color: #252525; line-height: 1.3em; } - { - position: absolute; - left: 3px; - top: 0; - display: block; - content:""; - background: #F89406; - height: 95%; - width: 2px; - z-index: 1; -} - .linklist-item-description a { text-decoration: none; color: #1b926c; @@ -618,32 +627,36 @@ body, .pure-g [class*="pure-u"] { .linklist-item-thumbnail { position: relative; - margin-top: 10px; - padding: 10px; - float: left; + padding: 0 0 0 5px; + margin: 0; + float: right; z-index: 50; + height: 90px; } .linklist-item.private .linklist-item-title::before, -.linklist-item.private .linklist-item-description::before, -.linklist-item.private .linklist-item-thumbnail::before { +.linklist-item.private .linklist-item-description::before { position: absolute; left: 3px; top: 0; display: block; content:""; background: #F89406; - height: 95%; + height: 96%; width: 2px; z-index: 1; } +.linklist-item.private .linklist-item-description::before { + height: 100%; +} + .linklist-item.private .linklist-item-title::before { margin-top: 3px; } .linklist-item-infos { - padding: 8px 8px 5px 8px; + padding: 4px 8px 4px 8px; background: #ddd; color: #252525; } @@ -680,6 +693,8 @@ body, .pure-g [class*="pure-u"] { overflow: hidden; text-overflow: ellipsis; font-size: 0.8em; + height:23px; + line-height:23px; } .linklist-item-infos .mobile-buttons { @@ -693,6 +708,16 @@ body, .pure-g [class*="pure-u"] { height: 16px; } +.linklist-item-infos-controls-group { + display: inline-block; + border-right: 1px solid #5d5d5d; + padding-right: 6px; +} + +.ctrl-edit { + margin: 0 7px; +} + /** 64em -> lg **/ @media screen and (max-width: 64em) { .linklist-item-infos-url { @@ -1284,3 +1309,22 @@ form[name="linkform"].page-form { text-decoration: none; font-weight: bold; } + +/** + * Markdown + */ +.markdown p { + margin: 0 !important; +} + +.markdown p + p { + margin: 0.5em 0 0 0 !important; +} + +.markdown *:first-child { + margin-top: 0 !important; +} + +.markdown *:last-child { + margin-bottom: 5px !important; +} \ No newline at end of file diff --git a/tpl/default/js/shaarli.js b/tpl/default/js/shaarli.js index 09b07eed..cf628e87 100644 --- a/tpl/default/js/shaarli.js +++ b/tpl/default/js/shaarli.js @@ -378,7 +378,7 @@ window.onload = function () { var linkCheckboxes = document.querySelectorAll('.delete-checkbox'); var bar = document.getElementById('actions'); [].forEach.call(linkCheckboxes, function(checkbox) { - checkbox.style.display = 'block'; + checkbox.style.display = 'inline-block'; checkbox.addEventListener('click', function(event) { var count = 0; var linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked'); diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html index 5dab8e9a..c666e30a 100644 --- a/tpl/default/linklist.html +++ b/tpl/default/linklist.html @@ -53,9 +53,9 @@ {/loop}