aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2017-10-22 13:19:51 +0200
committerGitHub <noreply@github.com>2017-10-22 13:19:51 +0200
commitd8acf8550480694d050091e270a06c01e7b313f0 (patch)
treef62befb2351c4b6550f71e11f6845bc3acb78a5d
parentefd3a6405a381501b070b8805ae37d1313969784 (diff)
parent1a47014f99d2f7aae00d37e62e0364d4eaa1ce29 (diff)
downloadShaarli-d8acf8550480694d050091e270a06c01e7b313f0.tar.gz
Shaarli-d8acf8550480694d050091e270a06c01e7b313f0.tar.zst
Shaarli-d8acf8550480694d050091e270a06c01e7b313f0.zip
Merge pull request #871 from ArthurHoaro/feature/translation
Shaarli's translation
-rw-r--r--.gitattributes1
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml2
-rw-r--r--Makefile17
-rw-r--r--application/ApplicationUtils.php19
-rw-r--r--application/Cache.php2
-rw-r--r--application/FeedBuilder.php4
-rw-r--r--application/History.php4
-rw-r--r--application/Languages.php167
-rw-r--r--application/LinkDB.php20
-rw-r--r--application/LinkFilter.php8
-rw-r--r--application/NetscapeBookmarkUtils.php15
-rw-r--r--application/PageBuilder.php7
-rw-r--r--application/PluginManager.php7
-rw-r--r--application/Updater.php8
-rw-r--r--application/Utils.php17
-rw-r--r--application/config/ConfigJson.php15
-rw-r--r--application/config/ConfigManager.php6
-rw-r--r--application/config/ConfigPhp.php4
-rw-r--r--application/config/exception/MissingFieldConfigException.php2
-rw-r--r--application/config/exception/PluginConfigOrderException.php2
-rw-r--r--application/config/exception/UnauthorizedConfigException.php2
-rw-r--r--application/exceptions/IOException.php2
-rw-r--r--composer.json3
-rw-r--r--composer.lock249
-rw-r--r--doc/md/Download-and-Installation.md4
-rw-r--r--doc/md/Server-requirements.md1
-rw-r--r--doc/md/Shaarli-configuration.md21
-rw-r--r--doc/md/Translations.md152
-rw-r--r--doc/md/Upgrade-and-migration.md21
-rw-r--r--doc/md/images/install-shaarli.pngbin0 -> 44376 bytes
-rw-r--r--doc/md/images/poedit-1.jpgbin0 -> 72956 bytes
-rw-r--r--inc/languages/fr/LC_MESSAGES/shaarli.po1366
-rw-r--r--index.php91
-rw-r--r--mkdocs.yml1
-rw-r--r--plugins/TODO.md28
-rw-r--r--plugins/addlink_toolbar/addlink_toolbar.php13
-rw-r--r--plugins/archiveorg/archiveorg.html6
-rw-r--r--plugins/archiveorg/archiveorg.php11
-rw-r--r--plugins/demo_plugin/demo_plugin.php37
-rw-r--r--plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mobin0 -> 652 bytes
-rw-r--r--plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po21
-rw-r--r--plugins/isso/isso.php17
-rw-r--r--plugins/markdown/help.html6
-rw-r--r--plugins/markdown/markdown.php21
-rw-r--r--plugins/piwik/piwik.php15
-rw-r--r--plugins/playvideos/playvideos.php13
-rw-r--r--plugins/pubsubhubbub/pubsubhubbub.php16
-rw-r--r--plugins/qrcode/qrcode.meta2
-rw-r--r--plugins/qrcode/qrcode.php9
-rw-r--r--plugins/wallabag/wallabag.html6
-rw-r--r--plugins/wallabag/wallabag.php20
-rw-r--r--tests/LanguagesTest.php186
-rw-r--r--tests/UtilsTest.php30
-rw-r--r--tests/bootstrap.php6
-rw-r--r--tests/languages/bootstrap.php7
-rw-r--r--tests/languages/fr/LanguagesFrTest.php175
-rw-r--r--tests/utils/languages/fr/LC_MESSAGES/test.mobin0 -> 456 bytes
-rw-r--r--tests/utils/languages/fr/LC_MESSAGES/test.po19
-rw-r--r--tpl/default/changetag.html2
-rw-r--r--tpl/default/configure.html24
-rw-r--r--tpl/default/import.html8
-rw-r--r--tpl/default/install.html21
-rw-r--r--tpl/default/js/shaarli.js11
-rw-r--r--tpl/default/linklist.html30
-rw-r--r--tpl/default/linklist.paging.html4
-rw-r--r--tpl/default/page.footer.html15
-rw-r--r--tpl/default/pluginsadmin.html4
68 files changed, 2744 insertions, 280 deletions
diff --git a/.gitattributes b/.gitattributes
index 93900602..b191e227 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -22,6 +22,7 @@ Dockerfile text
22*.ttf binary 22*.ttf binary
23*.min.css binary 23*.min.css binary
24*.min.js binary 24*.min.js binary
25*.mo binary
25 26
26# Exclude from Git archives 27# Exclude from Git archives
27.editorconfig export-ignore 28.editorconfig export-ignore
diff --git a/.gitignore b/.gitignore
index d546f248..3f6939a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,7 @@ vendor/
18# Release archives 18# Release archives
19*.tar.gz 19*.tar.gz
20*.zip 20*.zip
21inc/languages/*/LC_MESSAGES/shaarli.mo
21 22
22# Development and test resources 23# Development and test resources
23coverage 24coverage
diff --git a/.travis.yml b/.travis.yml
index b6b9bddf..322e4337 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,6 +13,8 @@ install:
13 - composer self-update 13 - composer self-update
14 - composer install --prefer-dist 14 - composer install --prefer-dist
15 - locale -a 15 - locale -a
16before_script:
17 - PATH=${PATH//:\.\/node_modules\/\.bin/}
16script: 18script:
17 - make clean 19 - make clean
18 - make check_permissions 20 - make check_permissions
diff --git a/Makefile b/Makefile
index 656c27b0..c2d55946 100644
--- a/Makefile
+++ b/Makefile
@@ -130,12 +130,12 @@ check_permissions:
130# See phpunit.xml for configuration 130# See phpunit.xml for configuration
131# https://phpunit.de/manual/current/en/appendixes.configuration.html 131# https://phpunit.de/manual/current/en/appendixes.configuration.html
132## 132##
133test: 133test: translate
134 @echo "-------" 134 @echo "-------"
135 @echo "PHPUNIT" 135 @echo "PHPUNIT"
136 @echo "-------" 136 @echo "-------"
137 @mkdir -p sandbox coverage 137 @mkdir -p sandbox coverage
138 @$(BIN)/phpunit --coverage-php coverage/main.cov --testsuite unit-tests 138 @$(BIN)/phpunit --coverage-php coverage/main.cov --bootstrap tests/bootstrap.php --testsuite unit-tests
139 139
140locale_test_%: 140locale_test_%:
141 @UT_LOCALE=$*.utf8 \ 141 @UT_LOCALE=$*.utf8 \
@@ -168,15 +168,15 @@ composer_dependencies: clean
168 composer install --no-dev --prefer-dist 168 composer install --no-dev --prefer-dist
169 find vendor/ -name ".git" -type d -exec rm -rf {} + 169 find vendor/ -name ".git" -type d -exec rm -rf {} +
170 170
171### generate a release tarball and include 3rd-party dependencies 171### generate a release tarball and include 3rd-party dependencies and translations
172release_tar: composer_dependencies htmldoc 172release_tar: composer_dependencies htmldoc translate
173 git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).tar HEAD 173 git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).tar HEAD
174 tar rvf $(ARCHIVE_VERSION).tar --transform "s|^vendor|$(ARCHIVE_PREFIX)vendor|" vendor/ 174 tar rvf $(ARCHIVE_VERSION).tar --transform "s|^vendor|$(ARCHIVE_PREFIX)vendor|" vendor/
175 tar rvf $(ARCHIVE_VERSION).tar --transform "s|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/ 175 tar rvf $(ARCHIVE_VERSION).tar --transform "s|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/
176 gzip $(ARCHIVE_VERSION).tar 176 gzip $(ARCHIVE_VERSION).tar
177 177
178### generate a release zip and include 3rd-party dependencies 178### generate a release zip and include 3rd-party dependencies and translations
179release_zip: composer_dependencies htmldoc 179release_zip: composer_dependencies htmldoc translate
180 git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD 180 git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD
181 mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor} 181 mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor}
182 rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/ 182 rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/
@@ -213,3 +213,8 @@ htmldoc:
213 mkdocs build' 213 mkdocs build'
214 find doc/html/ -type f -exec chmod a-x '{}' \; 214 find doc/html/ -type f -exec chmod a-x '{}' \;
215 rm -r venv 215 rm -r venv
216
217
218### Generate Shaarli's translation compiled file (.mo)
219translate:
220 @find inc/languages/ -name shaarli.po -execdir msgfmt shaarli.po -o shaarli.mo \; \ No newline at end of file
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 @@ class ApplicationUtils
149 public static function checkPHPVersion($minVersion, $curVersion) 149 public static function checkPHPVersion($minVersion, $curVersion)
150 { 150 {
151 if (version_compare($curVersion, $minVersion) < 0) { 151 if (version_compare($curVersion, $minVersion) < 0) {
152 throw new Exception( 152 $msg = t(
153 'Your PHP version is obsolete!' 153 'Your PHP version is obsolete!'
154 .' Shaarli requires at least PHP '.$minVersion.', and thus cannot run.' 154 . ' Shaarli requires at least PHP %s, and thus cannot run.'
155 .' Your PHP version has known security vulnerabilities and should be' 155 . ' Your PHP version has known security vulnerabilities and should be'
156 .' updated as soon as possible.' 156 . ' updated as soon as possible.'
157 ); 157 );
158 throw new Exception(sprintf($msg, $minVersion));
158 } 159 }
159 } 160 }
160 161
@@ -179,7 +180,7 @@ class ApplicationUtils
179 $rainTplDir.'/'.$conf->get('resource.theme'), 180 $rainTplDir.'/'.$conf->get('resource.theme'),
180 ) as $path) { 181 ) as $path) {
181 if (! is_readable(realpath($path))) { 182 if (! is_readable(realpath($path))) {
182 $errors[] = '"'.$path.'" directory is not readable'; 183 $errors[] = '"'.$path.'" '. t('directory is not readable');
183 } 184 }
184 } 185 }
185 186
@@ -191,10 +192,10 @@ class ApplicationUtils
191 $conf->get('resource.raintpl_tmp'), 192 $conf->get('resource.raintpl_tmp'),
192 ) as $path) { 193 ) as $path) {
193 if (! is_readable(realpath($path))) { 194 if (! is_readable(realpath($path))) {
194 $errors[] = '"'.$path.'" directory is not readable'; 195 $errors[] = '"'.$path.'" '. t('directory is not readable');
195 } 196 }
196 if (! is_writable(realpath($path))) { 197 if (! is_writable(realpath($path))) {
197 $errors[] = '"'.$path.'" directory is not writable'; 198 $errors[] = '"'.$path.'" '. t('directory is not writable');
198 } 199 }
199 } 200 }
200 201
@@ -212,10 +213,10 @@ class ApplicationUtils
212 } 213 }
213 214
214 if (! is_readable(realpath($path))) { 215 if (! is_readable(realpath($path))) {
215 $errors[] = '"'.$path.'" file is not readable'; 216 $errors[] = '"'.$path.'" '. t('file is not readable');
216 } 217 }
217 if (! is_writable(realpath($path))) { 218 if (! is_writable(realpath($path))) {
218 $errors[] = '"'.$path.'" file is not writable'; 219 $errors[] = '"'.$path.'" '. t('file is not writable');
219 } 220 }
220 } 221 }
221 222
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 @@
13function purgeCachedPages($pageCacheDir) 13function purgeCachedPages($pageCacheDir)
14{ 14{
15 if (! is_dir($pageCacheDir)) { 15 if (! is_dir($pageCacheDir)) {
16 $error = 'Cannot purge '.$pageCacheDir.': no directory'; 16 $error = sprintf(t('Cannot purge %s: no directory'), $pageCacheDir);
17 error_log($error); 17 error_log($error);
18 return $error; 18 return $error;
19 } 19 }
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 @@ class FeedBuilder
148 $link['url'] = $pageaddr . $link['url']; 148 $link['url'] = $pageaddr . $link['url'];
149 } 149 }
150 if ($this->usePermalinks === true) { 150 if ($this->usePermalinks === true) {
151 $permalink = '<a href="'. $link['url'] .'" title="Direct link">Direct link</a>'; 151 $permalink = '<a href="'. $link['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
152 } else { 152 } else {
153 $permalink = '<a href="'. $link['guid'] .'" title="Permalink">Permalink</a>'; 153 $permalink = '<a href="'. $link['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>';
154 } 154 }
155 $link['description'] = format_description($link['description'], '', $pageaddr); 155 $link['description'] = format_description($link['description'], '', $pageaddr);
156 $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink; 156 $link['description'] .= PHP_EOL .'<br>&#8212; '. $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 @@ class History
171 } 171 }
172 172
173 if (! is_writable($this->historyFilePath)) { 173 if (! is_writable($this->historyFilePath)) {
174 throw new Exception('History file isn\'t readable or writable'); 174 throw new Exception(t('History file isn\'t readable or writable'));
175 } 175 }
176 } 176 }
177 177
@@ -182,7 +182,7 @@ class History
182 { 182 {
183 $this->history = FileUtils::readFlatDB($this->historyFilePath, []); 183 $this->history = FileUtils::readFlatDB($this->historyFilePath, []);
184 if ($this->history === false) { 184 if ($this->history === false) {
185 throw new Exception('Could not parse history file'); 185 throw new Exception(t('Could not parse history file'));
186 } 186 }
187 } 187 }
188 188
diff --git a/application/Languages.php b/application/Languages.php
index c8b0a25a..357c7524 100644
--- a/application/Languages.php
+++ b/application/Languages.php
@@ -1,21 +1,164 @@
1<?php 1<?php
2 2
3namespace Shaarli;
4
5use Gettext\GettextTranslator;
6use Gettext\Merge;
7use Gettext\Translations;
8use Gettext\Translator;
9use Gettext\TranslatorInterface;
10use Shaarli\Config\ConfigManager;
11
3/** 12/**
4 * Wrapper function for translation which match the API 13 * Class Languages
5 * of gettext()/_() and ngettext(). 14 *
15 * Load Shaarli translations using 'gettext/gettext'.
16 * This class allows to either use PHP gettext extension, or a PHP implementation of gettext,
17 * with a fixed language, or dynamically using autoLocale().
6 * 18 *
7 * Not doing translation for now. 19 * Translation files PO/MO files follow gettext standard and must be placed under:
20 * <translation path>/<language>/LC_MESSAGES/<domain>.[po|mo]
8 * 21 *
9 * @param string $text Text to translate. 22 * Pros/cons:
10 * @param string $nText The plural message ID. 23 * - gettext extension is faster
11 * @param int $nb The number of items for plural forms. 24 * - gettext is very system dependent (PHP extension, the locale must be installed, and web server reloaded)
12 * 25 *
13 * @return String Text translated. 26 * Settings:
27 * - translation.mode:
28 * - auto: use default setting (PHP implementation)
29 * - php: use PHP implementation
30 * - gettext: use gettext wrapper
31 * - translation.language:
32 * - auto: use autoLocale() and the language change according to user HTTP headers
33 * - fixed language: e.g. 'fr'
34 * - translation.extensions:
35 * - domain => translation_path: allow plugins and themes to extend the defaut extension
36 * The domain must be unique, and translation path must be relative, and contains the tree mentioned above.
37 *
38 * @package Shaarli
14 */ 39 */
15function t($text, $nText = '', $nb = 0) { 40class Languages
16 if (empty($nText)) { 41{
17 return $text; 42 /**
43 * Core translations domain
44 */
45 const DEFAULT_DOMAIN = 'shaarli';
46
47 /**
48 * @var TranslatorInterface
49 */
50 protected $translator;
51
52 /**
53 * @var string
54 */
55 protected $language;
56
57 /**
58 * @var ConfigManager
59 */
60 protected $conf;
61
62 /**
63 * Languages constructor.
64 *
65 * @param string $language lang determined by autoLocale(), can be overridden.
66 * @param ConfigManager $conf instance.
67 */
68 public function __construct($language, $conf)
69 {
70 $this->conf = $conf;
71 $confLanguage = $this->conf->get('translation.language', 'auto');
72 if ($confLanguage === 'auto' || ! $this->isValidLanguage($confLanguage)) {
73 $this->language = substr($language, 0, 5);
74 } else {
75 $this->language = $confLanguage;
76 }
77
78 if (! extension_loaded('gettext')
79 || in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php'])
80 ) {
81 $this->initPhpTranslator();
82 } else {
83 $this->initGettextTranslator();
84 }
85
86 // Register default functions (e.g. '__()') to use our Translator
87 $this->translator->register();
88 }
89
90 /**
91 * Initialize the translator using php gettext extension (gettext dependency act as a wrapper).
92 */
93 protected function initGettextTranslator ()
94 {
95 $this->translator = new GettextTranslator();
96 $this->translator->setLanguage($this->language);
97 $this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages');
98
99 foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
100 if ($domain !== self::DEFAULT_DOMAIN) {
101 $this->translator->loadDomain($domain, $translationPath, false);
102 }
103 }
104 }
105
106 /**
107 * Initialize the translator using a PHP implementation of gettext.
108 *
109 * Note that if language po file doesn't exist, errors are ignored (e.g. not installed language).
110 */
111 protected function initPhpTranslator()
112 {
113 $this->translator = new Translator();
114 $translations = new Translations();
115 // Core translations
116 try {
117 /** @var Translations $translations */
118 $translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po');
119 $translations->setDomain('shaarli');
120 $this->translator->loadTranslations($translations);
121 } catch (\InvalidArgumentException $e) {}
122
123
124 // Extension translations (plugins, themes, etc.).
125 foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
126 if ($domain === self::DEFAULT_DOMAIN) {
127 continue;
128 }
129
130 try {
131 /** @var Translations $extension */
132 $extension = Translations::fromPoFile($translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po');
133 $extension->setDomain($domain);
134 $this->translator->loadTranslations($extension);
135 } catch (\InvalidArgumentException $e) {}
136 }
137 }
138
139 /**
140 * Checks if a language string is valid.
141 *
142 * @param string $language e.g. 'fr' or 'en_US'
143 *
144 * @return bool true if valid, false otherwise
145 */
146 protected function isValidLanguage($language)
147 {
148 return preg_match('/^[a-z]{2}(_[A-Z]{2})?/', $language) === 1;
149 }
150
151 /**
152 * Get the list of available languages for Shaarli.
153 *
154 * @return array List of available languages, with their label.
155 */
156 public static function getAvailableLanguages()
157 {
158 return [
159 'auto' => t('Automatic'),
160 'en' => t('English'),
161 'fr' => t('French'),
162 ];
18 } 163 }
19 $actualForm = $nb > 1 ? $nText : $text;
20 return sprintf($actualForm, $nb);
21} 164}
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 @@ class LinkDB implements Iterator, Countable, ArrayAccess
133 { 133 {
134 // TODO: use exceptions instead of "die" 134 // TODO: use exceptions instead of "die"
135 if (!$this->loggedIn) { 135 if (!$this->loggedIn) {
136 die('You are not authorized to add a link.'); 136 die(t('You are not authorized to add a link.'));
137 } 137 }
138 if (!isset($value['id']) || empty($value['url'])) { 138 if (!isset($value['id']) || empty($value['url'])) {
139 die('Internal Error: A link should always have an id and URL.'); 139 die(t('Internal Error: A link should always have an id and URL.'));
140 } 140 }
141 if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) { 141 if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) {
142 die('You must specify an integer as a key.'); 142 die(t('You must specify an integer as a key.'));
143 } 143 }
144 if ($offset !== null && $offset !== $value['id']) { 144 if ($offset !== null && $offset !== $value['id']) {
145 die('Array offset and link ID must be equal.'); 145 die(t('Array offset and link ID must be equal.'));
146 } 146 }
147 147
148 // If the link exists, we reuse the real offset, otherwise new entry 148 // If the link exists, we reuse the real offset, otherwise new entry
@@ -248,13 +248,13 @@ class LinkDB implements Iterator, Countable, ArrayAccess
248 $this->links = array(); 248 $this->links = array();
249 $link = array( 249 $link = array(
250 'id' => 1, 250 'id' => 1,
251 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', 251 'title'=> t('The personal, minimalist, super-fast, database free, bookmarking service'),
252 'url'=>'https://shaarli.readthedocs.io', 252 'url'=>'https://shaarli.readthedocs.io',
253 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login. 253 'description'=>t('Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
254 254
255To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page. 255To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page.
256 256
257You use the community supported version of the original Shaarli project, by Sebastien Sauvage.', 257You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'),
258 'private'=>0, 258 'private'=>0,
259 'created'=> new DateTime(), 259 'created'=> new DateTime(),
260 'tags'=>'opensource software' 260 'tags'=>'opensource software'
@@ -264,9 +264,9 @@ You use the community supported version of the original Shaarli project, by Seba
264 264
265 $link = array( 265 $link = array(
266 'id' => 0, 266 'id' => 0,
267 'title'=>'My secret stuff... - Pastebin.com', 267 'title'=> t('My secret stuff... - Pastebin.com'),
268 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', 268 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
269 'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.', 269 'description'=> t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'),
270 'private'=>1, 270 'private'=>1,
271 'created'=> new DateTime('1 minute ago'), 271 'created'=> new DateTime('1 minute ago'),
272 'tags'=>'secretstuff', 272 '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 @@ class LinkFilter
444 444
445class LinkNotFoundException extends Exception 445class LinkNotFoundException extends Exception
446{ 446{
447 protected $message = 'The link you are trying to reach does not exist or has been deleted.'; 447 /**
448 * LinkNotFoundException constructor.
449 */
450 public function __construct()
451 {
452 $this->message = t('The link you are trying to reach does not exist or has been deleted.');
453 }
448} 454}
diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php
index 31796367..dd7057f8 100644
--- a/application/NetscapeBookmarkUtils.php
+++ b/application/NetscapeBookmarkUtils.php
@@ -32,11 +32,10 @@ class NetscapeBookmarkUtils
32 { 32 {
33 // see tpl/export.html for possible values 33 // see tpl/export.html for possible values
34 if (! in_array($selection, array('all', 'public', 'private'))) { 34 if (! in_array($selection, array('all', 'public', 'private'))) {
35 throw new Exception('Invalid export selection: "'.$selection.'"'); 35 throw new Exception(t('Invalid export selection:') .' "'.$selection.'"');
36 } 36 }
37 37
38 $bookmarkLinks = array(); 38 $bookmarkLinks = array();
39
40 foreach ($linkDb as $link) { 39 foreach ($linkDb as $link) {
41 if ($link['private'] != 0 && $selection == 'public') { 40 if ($link['private'] != 0 && $selection == 'public') {
42 continue; 41 continue;
@@ -79,14 +78,14 @@ class NetscapeBookmarkUtils
79 $duration=0 78 $duration=0
80 ) 79 )
81 { 80 {
82 $status = 'File '.$filename.' ('.$filesize.' bytes) '; 81 $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
83 if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) { 82 if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
84 $status .= 'has an unknown file format. Nothing was imported.'; 83 $status .= t('has an unknown file format. Nothing was imported.');
85 } else { 84 } else {
86 $status .= 'was successfully processed in '. $duration .' seconds: '; 85 $status .= vsprintf(
87 $status .= $importCount.' links imported, '; 86 t('was successfully processed in %d seconds: %d links imported, %d links overwritten, %d links skipped.'),
88 $status .= $overwriteCount.' links overwritten, '; 87 [$duration, $importCount, $overwriteCount, $skipCount]
89 $status .= $skipCount.' links skipped.'; 88 );
90 } 89 }
91 return $status; 90 return $status;
92 } 91 }
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 @@ class PageBuilder
159 * 159 *
160 * @param string $message A messate to display what is not found 160 * @param string $message A messate to display what is not found
161 */ 161 */
162 public function render404($message = 'The page you are trying to reach does not exist or has been deleted.') 162 public function render404($message = '')
163 { 163 {
164 header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); 164 if (empty($message)) {
165 $message = t('The page you are trying to reach does not exist or has been deleted.');
166 }
167 header($_SERVER['SERVER_PROTOCOL'] .' '. t('404 Not Found'));
165 $this->tpl->assign('error_message', $message); 168 $this->tpl->assign('error_message', $message);
166 $this->renderPage('404'); 169 $this->renderPage('404');
167 } 170 }
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 @@ class PluginManager
188 $metaData[$plugin] = parse_ini_file($metaFile); 188 $metaData[$plugin] = parse_ini_file($metaFile);
189 $metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins); 189 $metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins);
190 190
191 if (isset($metaData[$plugin]['description'])) {
192 $metaData[$plugin]['description'] = t($metaData[$plugin]['description']);
193 }
191 // Read parameters and format them into an array. 194 // Read parameters and format them into an array.
192 if (isset($metaData[$plugin]['parameters'])) { 195 if (isset($metaData[$plugin]['parameters'])) {
193 $params = explode(';', $metaData[$plugin]['parameters']); 196 $params = explode(';', $metaData[$plugin]['parameters']);
@@ -203,7 +206,7 @@ class PluginManager
203 $metaData[$plugin]['parameters'][$param]['value'] = ''; 206 $metaData[$plugin]['parameters'][$param]['value'] = '';
204 // Optional parameter description in parameter.PARAM_NAME= 207 // Optional parameter description in parameter.PARAM_NAME=
205 if (isset($metaData[$plugin]['parameter.'. $param])) { 208 if (isset($metaData[$plugin]['parameter.'. $param])) {
206 $metaData[$plugin]['parameters'][$param]['desc'] = $metaData[$plugin]['parameter.'. $param]; 209 $metaData[$plugin]['parameters'][$param]['desc'] = t($metaData[$plugin]['parameter.'. $param]);
207 } 210 }
208 } 211 }
209 } 212 }
@@ -237,6 +240,6 @@ class PluginFileNotFoundException extends Exception
237 */ 240 */
238 public function __construct($pluginName) 241 public function __construct($pluginName)
239 { 242 {
240 $this->message = 'Plugin "'. $pluginName .'" files not found.'; 243 $this->message = sprintf(t('Plugin "%s" files not found.'), $pluginName);
241 } 244 }
242} 245}
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 @@ class Updater
73 } 73 }
74 74
75 if ($this->methods === null) { 75 if ($this->methods === null) {
76 throw new UpdaterException('Couldn\'t retrieve Updater class methods.'); 76 throw new UpdaterException(t('Couldn\'t retrieve Updater class methods.'));
77 } 77 }
78 78
79 foreach ($this->methods as $method) { 79 foreach ($this->methods as $method) {
@@ -482,7 +482,7 @@ class UpdaterException extends Exception
482 } 482 }
483 483
484 if (! empty($this->method)) { 484 if (! empty($this->method)) {
485 $out .= 'An error occurred while running the update '. $this->method . PHP_EOL; 485 $out .= t('An error occurred while running the update ') . $this->method . PHP_EOL;
486 } 486 }
487 487
488 if (! empty($this->previous)) { 488 if (! empty($this->previous)) {
@@ -522,11 +522,11 @@ function read_updates_file($updatesFilepath)
522function write_updates_file($updatesFilepath, $updates) 522function write_updates_file($updatesFilepath, $updates)
523{ 523{
524 if (empty($updatesFilepath)) { 524 if (empty($updatesFilepath)) {
525 throw new Exception('Updates file path is not set, can\'t write updates.'); 525 throw new Exception(t('Updates file path is not set, can\'t write updates.'));
526 } 526 }
527 527
528 $res = file_put_contents($updatesFilepath, implode(';', $updates)); 528 $res = file_put_contents($updatesFilepath, implode(';', $updates));
529 if ($res === false) { 529 if ($res === false) {
530 throw new Exception('Unable to write updates in '. $updatesFilepath . '.'); 530 throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.'));
531 } 531 }
532} 532}
diff --git a/application/Utils.php b/application/Utils.php
index 4a2f5561..2f38a8de 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -452,7 +452,7 @@ function get_max_upload_size($limitPost, $limitUpload, $format = true)
452 */ 452 */
453function alphabetical_sort(&$data, $reverse = false, $byKeys = false) 453function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
454{ 454{
455 $callback = function($a, $b) use ($reverse) { 455 $callback = function ($a, $b) use ($reverse) {
456 // Collator is part of PHP intl. 456 // Collator is part of PHP intl.
457 if (class_exists('Collator')) { 457 if (class_exists('Collator')) {
458 $collator = new Collator(setlocale(LC_COLLATE, 0)); 458 $collator = new Collator(setlocale(LC_COLLATE, 0));
@@ -470,3 +470,18 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
470 usort($data, $callback); 470 usort($data, $callback);
471 } 471 }
472} 472}
473
474/**
475 * Wrapper function for translation which match the API
476 * of gettext()/_() and ngettext().
477 *
478 * @param string $text Text to translate.
479 * @param string $nText The plural message ID.
480 * @param int $nb The number of items for plural forms.
481 * @param string $domain The domain where the translation is stored (default: shaarli).
482 *
483 * @return string Text translated.
484 */
485function t($text, $nText = '', $nb = 1, $domain = 'shaarli') {
486 return dn__($domain, $text, $nText, $nb);
487}
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 @@ class ConfigJson implements ConfigIO
22 $data = json_decode($data, true); 22 $data = json_decode($data, true);
23 if ($data === null) { 23 if ($data === null) {
24 $errorCode = json_last_error(); 24 $errorCode = json_last_error();
25 $error = 'An error occurred while parsing JSON configuration file ('. $filepath .'): error code #'; 25 $error = sprintf(
26 $error .= $errorCode. '<br>➜ <code>' . json_last_error_msg() .'</code>'; 26 'An error occurred while parsing JSON configuration file (%s): error code #%d',
27 $filepath,
28 $errorCode
29 );
30 $error .= '<br>➜ <code>' . json_last_error_msg() .'</code>';
27 if ($errorCode === JSON_ERROR_SYNTAX) { 31 if ($errorCode === JSON_ERROR_SYNTAX) {
28 $error .= '<br>Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as '; 32 $error .= '<br>';
33 $error .= 'Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as ';
29 $error .= '<a href="http://jsonlint.com/">jsonlint.com</a>.'; 34 $error .= '<a href="http://jsonlint.com/">jsonlint.com</a>.';
30 } 35 }
31 throw new \Exception($error); 36 throw new \Exception($error);
@@ -44,8 +49,8 @@ class ConfigJson implements ConfigIO
44 if (!file_put_contents($filepath, $data)) { 49 if (!file_put_contents($filepath, $data)) {
45 throw new \IOException( 50 throw new \IOException(
46 $filepath, 51 $filepath,
47 'Shaarli could not create the config file. 52 t('Shaarli could not create the config file. '.
48 Please make sure Shaarli has the right to write in the folder is it installed in.' 53 'Please make sure Shaarli has the right to write in the folder is it installed in.')
49 ); 54 );
50 } 55 }
51 } 56 }
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 @@ class ConfigManager
132 public function set($setting, $value, $write = false, $isLoggedIn = false) 132 public function set($setting, $value, $write = false, $isLoggedIn = false)
133 { 133 {
134 if (empty($setting) || ! is_string($setting)) { 134 if (empty($setting) || ! is_string($setting)) {
135 throw new \Exception('Invalid setting key parameter. String expected, got: '. gettype($setting)); 135 throw new \Exception(t('Invalid setting key parameter. String expected, got: '). gettype($setting));
136 } 136 }
137 137
138 // During the ConfigIO transition, map legacy settings to the new ones. 138 // During the ConfigIO transition, map legacy settings to the new ones.
@@ -339,6 +339,10 @@ class ConfigManager
339 $this->setEmpty('redirector.url', ''); 339 $this->setEmpty('redirector.url', '');
340 $this->setEmpty('redirector.encode_url', true); 340 $this->setEmpty('redirector.encode_url', true);
341 341
342 $this->setEmpty('translation.language', 'auto');
343 $this->setEmpty('translation.mode', 'php');
344 $this->setEmpty('translation.extensions', []);
345
342 $this->setEmpty('plugins', array()); 346 $this->setEmpty('plugins', array());
343 } 347 }
344 348
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 @@ class ConfigPhp implements ConfigIO
118 ) { 118 ) {
119 throw new \IOException( 119 throw new \IOException(
120 $filepath, 120 $filepath,
121 'Shaarli could not create the config file. 121 t('Shaarli could not create the config file. '.
122 Please make sure Shaarli has the right to write in the folder is it installed in.' 122 'Please make sure Shaarli has the right to write in the folder is it installed in.')
123 ); 123 );
124 } 124 }
125 } 125 }
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
18 public function __construct($field) 18 public function __construct($field)
19 { 19 {
20 $this->field = $field; 20 $this->field = $field;
21 $this->message = 'Configuration value is required for '. $this->field; 21 $this->message = sprintf(t('Configuration value is required for %s'), $this->field);
22 } 22 }
23} 23}
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
12 */ 12 */
13 public function __construct() 13 public function __construct()
14 { 14 {
15 $this->message = 'An error occurred while trying to save plugins loading order.'; 15 $this->message = t('An error occurred while trying to save plugins loading order.');
16 } 16 }
17} 17}
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
13 */ 13 */
14 public function __construct() 14 public function __construct()
15 { 15 {
16 $this->message = 'You are not authorized to alter config.'; 16 $this->message = t('You are not authorized to alter config.');
17 } 17 }
18} 18}
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
16 public function __construct($path, $message = '') 16 public function __construct($path, $message = '')
17 { 17 {
18 $this->path = $path; 18 $this->path = $path;
19 $this->message = empty($message) ? 'Error accessing' : $message; 19 $this->message = empty($message) ? t('Error accessing') : $message;
20 $this->message .= ' "' . $this->path .'"'; 20 $this->message .= ' "' . $this->path .'"';
21 } 21 }
22} 22}
diff --git a/composer.json b/composer.json
index afb8aca4..f331d6ca 100644
--- a/composer.json
+++ b/composer.json
@@ -19,7 +19,8 @@
19 "shaarli/netscape-bookmark-parser": "^2.0", 19 "shaarli/netscape-bookmark-parser": "^2.0",
20 "erusev/parsedown": "1.6", 20 "erusev/parsedown": "1.6",
21 "slim/slim": "^3.0", 21 "slim/slim": "^3.0",
22 "pubsubhubbub/publisher": "dev-master" 22 "pubsubhubbub/publisher": "dev-master",
23 "gettext/gettext": "^4.4"
23 }, 24 },
24 "require-dev": { 25 "require-dev": {
25 "phpmd/phpmd" : "@stable", 26 "phpmd/phpmd" : "@stable",
diff --git a/composer.lock b/composer.lock
index 435d6a88..39909b8f 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
5 "This file is @generated automatically" 5 "This file is @generated automatically"
6 ], 6 ],
7 "content-hash": "68beedbfa104c788029b079800cfd6e8", 7 "content-hash": "13b7e1e474fe9264b098ba86face0feb",
8 "packages": [ 8 "packages": [
9 { 9 {
10 "name": "container-interop/container-interop", 10 "name": "container-interop/container-interop",
@@ -77,6 +77,129 @@
77 "time": "2015-10-04T16:44:32+00:00" 77 "time": "2015-10-04T16:44:32+00:00"
78 }, 78 },
79 { 79 {
80 "name": "gettext/gettext",
81 "version": "v4.4.3",
82 "source": {
83 "type": "git",
84 "url": "https://github.com/oscarotero/Gettext.git",
85 "reference": "4f57f004635cc6311a20815ebfdc0757cb337113"
86 },
87 "dist": {
88 "type": "zip",
89 "url": "https://api.github.com/repos/oscarotero/Gettext/zipball/4f57f004635cc6311a20815ebfdc0757cb337113",
90 "reference": "4f57f004635cc6311a20815ebfdc0757cb337113",
91 "shasum": ""
92 },
93 "require": {
94 "gettext/languages": "^2.3",
95 "php": ">=5.4.0"
96 },
97 "require-dev": {
98 "illuminate/view": "*",
99 "phpunit/phpunit": "^4.8|^5.7",
100 "squizlabs/php_codesniffer": "^3.0",
101 "symfony/yaml": "~2",
102 "twig/extensions": "*",
103 "twig/twig": "^1.31|^2.0"
104 },
105 "suggest": {
106 "illuminate/view": "Is necessary if you want to use the Blade extractor",
107 "symfony/yaml": "Is necessary if you want to use the Yaml extractor/generator",
108 "twig/extensions": "Is necessary if you want to use the Twig extractor",
109 "twig/twig": "Is necessary if you want to use the Twig extractor"
110 },
111 "type": "library",
112 "autoload": {
113 "psr-4": {
114 "Gettext\\": "src"
115 }
116 },
117 "notification-url": "https://packagist.org/downloads/",
118 "license": [
119 "MIT"
120 ],
121 "authors": [
122 {
123 "name": "Oscar Otero",
124 "email": "oom@oscarotero.com",
125 "homepage": "http://oscarotero.com",
126 "role": "Developer"
127 }
128 ],
129 "description": "PHP gettext manager",
130 "homepage": "https://github.com/oscarotero/Gettext",
131 "keywords": [
132 "JS",
133 "gettext",
134 "i18n",
135 "mo",
136 "po",
137 "translation"
138 ],
139 "time": "2017-08-09T16:59:46+00:00"
140 },
141 {
142 "name": "gettext/languages",
143 "version": "2.3.0",
144 "source": {
145 "type": "git",
146 "url": "https://github.com/mlocati/cldr-to-gettext-plural-rules.git",
147 "reference": "49c39e51569963cc917a924b489e7025bfb9d8c7"
148 },
149 "dist": {
150 "type": "zip",
151 "url": "https://api.github.com/repos/mlocati/cldr-to-gettext-plural-rules/zipball/49c39e51569963cc917a924b489e7025bfb9d8c7",
152 "reference": "49c39e51569963cc917a924b489e7025bfb9d8c7",
153 "shasum": ""
154 },
155 "require": {
156 "php": ">=5.3"
157 },
158 "require-dev": {
159 "phpunit/phpunit": "^4"
160 },
161 "bin": [
162 "bin/export-plural-rules",
163 "bin/export-plural-rules.php"
164 ],
165 "type": "library",
166 "autoload": {
167 "psr-4": {
168 "Gettext\\Languages\\": "src/"
169 }
170 },
171 "notification-url": "https://packagist.org/downloads/",
172 "license": [
173 "MIT"
174 ],
175 "authors": [
176 {
177 "name": "Michele Locati",
178 "email": "mlocati@gmail.com",
179 "role": "Developer"
180 }
181 ],
182 "description": "gettext languages with plural rules",
183 "homepage": "https://github.com/mlocati/cldr-to-gettext-plural-rules",
184 "keywords": [
185 "cldr",
186 "i18n",
187 "internationalization",
188 "l10n",
189 "language",
190 "languages",
191 "localization",
192 "php",
193 "plural",
194 "plural rules",
195 "plurals",
196 "translate",
197 "translations",
198 "unicode"
199 ],
200 "time": "2017-03-23T17:02:28+00:00"
201 },
202 {
80 "name": "katzgrau/klogger", 203 "name": "katzgrau/klogger",
81 "version": "1.2.1", 204 "version": "1.2.1",
82 "source": { 205 "source": {
@@ -371,12 +494,12 @@
371 "source": { 494 "source": {
372 "type": "git", 495 "type": "git",
373 "url": "https://github.com/pubsubhubbub/php-publisher.git", 496 "url": "https://github.com/pubsubhubbub/php-publisher.git",
374 "reference": "a5d6a0e1cc9d49101c3904480e5b06cbb8addba7" 497 "reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f"
375 }, 498 },
376 "dist": { 499 "dist": {
377 "type": "zip", 500 "type": "zip",
378 "url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/a5d6a0e1cc9d49101c3904480e5b06cbb8addba7", 501 "url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/0d224daebd504ab61c22fee4db58f8d1fc18945f",
379 "reference": "a5d6a0e1cc9d49101c3904480e5b06cbb8addba7", 502 "reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f",
380 "shasum": "" 503 "shasum": ""
381 }, 504 },
382 "require": { 505 "require": {
@@ -406,7 +529,7 @@
406 "publishers", 529 "publishers",
407 "pubsubhubbub" 530 "pubsubhubbub"
408 ], 531 ],
409 "time": "2016-11-15T06:24:01+00:00" 532 "time": "2017-10-08T10:59:41+00:00"
410 }, 533 },
411 { 534 {
412 "name": "shaarli/netscape-bookmark-parser", 535 "name": "shaarli/netscape-bookmark-parser",
@@ -632,16 +755,16 @@
632 }, 755 },
633 { 756 {
634 "name": "phpdocumentor/reflection-common", 757 "name": "phpdocumentor/reflection-common",
635 "version": "1.0", 758 "version": "1.0.1",
636 "source": { 759 "source": {
637 "type": "git", 760 "type": "git",
638 "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 761 "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
639 "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" 762 "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
640 }, 763 },
641 "dist": { 764 "dist": {
642 "type": "zip", 765 "type": "zip",
643 "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", 766 "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
644 "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", 767 "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
645 "shasum": "" 768 "shasum": ""
646 }, 769 },
647 "require": { 770 "require": {
@@ -682,20 +805,20 @@
682 "reflection", 805 "reflection",
683 "static analysis" 806 "static analysis"
684 ], 807 ],
685 "time": "2015-12-27T11:43:31+00:00" 808 "time": "2017-09-11T18:02:19+00:00"
686 }, 809 },
687 { 810 {
688 "name": "phpdocumentor/reflection-docblock", 811 "name": "phpdocumentor/reflection-docblock",
689 "version": "3.2.1", 812 "version": "3.2.2",
690 "source": { 813 "source": {
691 "type": "git", 814 "type": "git",
692 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 815 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
693 "reference": "183824db76118b9dddffc7e522b91fa175f75119" 816 "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157"
694 }, 817 },
695 "dist": { 818 "dist": {
696 "type": "zip", 819 "type": "zip",
697 "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/183824db76118b9dddffc7e522b91fa175f75119", 820 "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157",
698 "reference": "183824db76118b9dddffc7e522b91fa175f75119", 821 "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157",
699 "shasum": "" 822 "shasum": ""
700 }, 823 },
701 "require": { 824 "require": {
@@ -727,7 +850,7 @@
727 } 850 }
728 ], 851 ],
729 "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 852 "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
730 "time": "2017-08-04T20:55:59+00:00" 853 "time": "2017-08-08T06:39:58+00:00"
731 }, 854 },
732 { 855 {
733 "name": "phpdocumentor/type-resolver", 856 "name": "phpdocumentor/type-resolver",
@@ -844,22 +967,22 @@
844 }, 967 },
845 { 968 {
846 "name": "phpspec/prophecy", 969 "name": "phpspec/prophecy",
847 "version": "v1.7.0", 970 "version": "v1.7.2",
848 "source": { 971 "source": {
849 "type": "git", 972 "type": "git",
850 "url": "https://github.com/phpspec/prophecy.git", 973 "url": "https://github.com/phpspec/prophecy.git",
851 "reference": "93d39f1f7f9326d746203c7c056f300f7f126073" 974 "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6"
852 }, 975 },
853 "dist": { 976 "dist": {
854 "type": "zip", 977 "type": "zip",
855 "url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073", 978 "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
856 "reference": "93d39f1f7f9326d746203c7c056f300f7f126073", 979 "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
857 "shasum": "" 980 "shasum": ""
858 }, 981 },
859 "require": { 982 "require": {
860 "doctrine/instantiator": "^1.0.2", 983 "doctrine/instantiator": "^1.0.2",
861 "php": "^5.3|^7.0", 984 "php": "^5.3|^7.0",
862 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", 985 "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
863 "sebastian/comparator": "^1.1|^2.0", 986 "sebastian/comparator": "^1.1|^2.0",
864 "sebastian/recursion-context": "^1.0|^2.0|^3.0" 987 "sebastian/recursion-context": "^1.0|^2.0|^3.0"
865 }, 988 },
@@ -870,7 +993,7 @@
870 "type": "library", 993 "type": "library",
871 "extra": { 994 "extra": {
872 "branch-alias": { 995 "branch-alias": {
873 "dev-master": "1.6.x-dev" 996 "dev-master": "1.7.x-dev"
874 } 997 }
875 }, 998 },
876 "autoload": { 999 "autoload": {
@@ -903,7 +1026,7 @@
903 "spy", 1026 "spy",
904 "stub" 1027 "stub"
905 ], 1028 ],
906 "time": "2017-03-02T20:05:34+00:00" 1029 "time": "2017-09-04T11:05:03+00:00"
907 }, 1030 },
908 { 1031 {
909 "name": "phpunit/php-code-coverage", 1032 "name": "phpunit/php-code-coverage",
@@ -1875,20 +1998,20 @@
1875 }, 1998 },
1876 { 1999 {
1877 "name": "symfony/config", 2000 "name": "symfony/config",
1878 "version": "v3.3.6", 2001 "version": "v3.3.10",
1879 "source": { 2002 "source": {
1880 "type": "git", 2003 "type": "git",
1881 "url": "https://github.com/symfony/config.git", 2004 "url": "https://github.com/symfony/config.git",
1882 "reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297" 2005 "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd"
1883 }, 2006 },
1884 "dist": { 2007 "dist": {
1885 "type": "zip", 2008 "type": "zip",
1886 "url": "https://api.github.com/repos/symfony/config/zipball/54ee12b0dd60f294132cabae6f5da9573d2e5297", 2009 "url": "https://api.github.com/repos/symfony/config/zipball/4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
1887 "reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297", 2010 "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
1888 "shasum": "" 2011 "shasum": ""
1889 }, 2012 },
1890 "require": { 2013 "require": {
1891 "php": ">=5.5.9", 2014 "php": "^5.5.9|>=7.0.8",
1892 "symfony/filesystem": "~2.8|~3.0" 2015 "symfony/filesystem": "~2.8|~3.0"
1893 }, 2016 },
1894 "conflict": { 2017 "conflict": {
@@ -1933,20 +2056,20 @@
1933 ], 2056 ],
1934 "description": "Symfony Config Component", 2057 "description": "Symfony Config Component",
1935 "homepage": "https://symfony.com", 2058 "homepage": "https://symfony.com",
1936 "time": "2017-07-19T07:37:29+00:00" 2059 "time": "2017-10-04T18:56:58+00:00"
1937 }, 2060 },
1938 { 2061 {
1939 "name": "symfony/console", 2062 "name": "symfony/console",
1940 "version": "v2.8.26", 2063 "version": "v2.8.28",
1941 "source": { 2064 "source": {
1942 "type": "git", 2065 "type": "git",
1943 "url": "https://github.com/symfony/console.git", 2066 "url": "https://github.com/symfony/console.git",
1944 "reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd" 2067 "reference": "f81549d2c5fdee8d711c9ab3c7e7362353ea5853"
1945 }, 2068 },
1946 "dist": { 2069 "dist": {
1947 "type": "zip", 2070 "type": "zip",
1948 "url": "https://api.github.com/repos/symfony/console/zipball/32a3c6b3398de5db8ed381f4ef92970c59c2fcdd", 2071 "url": "https://api.github.com/repos/symfony/console/zipball/f81549d2c5fdee8d711c9ab3c7e7362353ea5853",
1949 "reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd", 2072 "reference": "f81549d2c5fdee8d711c9ab3c7e7362353ea5853",
1950 "shasum": "" 2073 "shasum": ""
1951 }, 2074 },
1952 "require": { 2075 "require": {
@@ -1994,7 +2117,7 @@
1994 ], 2117 ],
1995 "description": "Symfony Console Component", 2118 "description": "Symfony Console Component",
1996 "homepage": "https://symfony.com", 2119 "homepage": "https://symfony.com",
1997 "time": "2017-07-29T21:26:04+00:00" 2120 "time": "2017-10-01T21:00:16+00:00"
1998 }, 2121 },
1999 { 2122 {
2000 "name": "symfony/debug", 2123 "name": "symfony/debug",
@@ -2055,20 +2178,20 @@
2055 }, 2178 },
2056 { 2179 {
2057 "name": "symfony/dependency-injection", 2180 "name": "symfony/dependency-injection",
2058 "version": "v3.3.6", 2181 "version": "v3.3.10",
2059 "source": { 2182 "source": {
2060 "type": "git", 2183 "type": "git",
2061 "url": "https://github.com/symfony/dependency-injection.git", 2184 "url": "https://github.com/symfony/dependency-injection.git",
2062 "reference": "8d70987f991481e809c63681ffe8ce3f3fde68a0" 2185 "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1"
2063 }, 2186 },
2064 "dist": { 2187 "dist": {
2065 "type": "zip", 2188 "type": "zip",
2066 "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8d70987f991481e809c63681ffe8ce3f3fde68a0", 2189 "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8ebad929aee3ca185b05f55d9cc5521670821ad1",
2067 "reference": "8d70987f991481e809c63681ffe8ce3f3fde68a0", 2190 "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1",
2068 "shasum": "" 2191 "shasum": ""
2069 }, 2192 },
2070 "require": { 2193 "require": {
2071 "php": ">=5.5.9", 2194 "php": "^5.5.9|>=7.0.8",
2072 "psr/container": "^1.0" 2195 "psr/container": "^1.0"
2073 }, 2196 },
2074 "conflict": { 2197 "conflict": {
@@ -2121,24 +2244,24 @@
2121 ], 2244 ],
2122 "description": "Symfony DependencyInjection Component", 2245 "description": "Symfony DependencyInjection Component",
2123 "homepage": "https://symfony.com", 2246 "homepage": "https://symfony.com",
2124 "time": "2017-07-28T15:27:31+00:00" 2247 "time": "2017-10-04T17:15:30+00:00"
2125 }, 2248 },
2126 { 2249 {
2127 "name": "symfony/filesystem", 2250 "name": "symfony/filesystem",
2128 "version": "v3.3.6", 2251 "version": "v3.3.10",
2129 "source": { 2252 "source": {
2130 "type": "git", 2253 "type": "git",
2131 "url": "https://github.com/symfony/filesystem.git", 2254 "url": "https://github.com/symfony/filesystem.git",
2132 "reference": "427987eb4eed764c3b6e38d52a0f87989e010676" 2255 "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1"
2133 }, 2256 },
2134 "dist": { 2257 "dist": {
2135 "type": "zip", 2258 "type": "zip",
2136 "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676", 2259 "url": "https://api.github.com/repos/symfony/filesystem/zipball/90bc45abf02ae6b7deb43895c1052cb0038506f1",
2137 "reference": "427987eb4eed764c3b6e38d52a0f87989e010676", 2260 "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1",
2138 "shasum": "" 2261 "shasum": ""
2139 }, 2262 },
2140 "require": { 2263 "require": {
2141 "php": ">=5.5.9" 2264 "php": "^5.5.9|>=7.0.8"
2142 }, 2265 },
2143 "type": "library", 2266 "type": "library",
2144 "extra": { 2267 "extra": {
@@ -2170,24 +2293,24 @@
2170 ], 2293 ],
2171 "description": "Symfony Filesystem Component", 2294 "description": "Symfony Filesystem Component",
2172 "homepage": "https://symfony.com", 2295 "homepage": "https://symfony.com",
2173 "time": "2017-07-11T07:17:58+00:00" 2296 "time": "2017-10-03T13:33:10+00:00"
2174 }, 2297 },
2175 { 2298 {
2176 "name": "symfony/finder", 2299 "name": "symfony/finder",
2177 "version": "v3.3.6", 2300 "version": "v3.3.10",
2178 "source": { 2301 "source": {
2179 "type": "git", 2302 "type": "git",
2180 "url": "https://github.com/symfony/finder.git", 2303 "url": "https://github.com/symfony/finder.git",
2181 "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4" 2304 "reference": "773e19a491d97926f236942484cb541560ce862d"
2182 }, 2305 },
2183 "dist": { 2306 "dist": {
2184 "type": "zip", 2307 "type": "zip",
2185 "url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4", 2308 "url": "https://api.github.com/repos/symfony/finder/zipball/773e19a491d97926f236942484cb541560ce862d",
2186 "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4", 2309 "reference": "773e19a491d97926f236942484cb541560ce862d",
2187 "shasum": "" 2310 "shasum": ""
2188 }, 2311 },
2189 "require": { 2312 "require": {
2190 "php": ">=5.5.9" 2313 "php": "^5.5.9|>=7.0.8"
2191 }, 2314 },
2192 "type": "library", 2315 "type": "library",
2193 "extra": { 2316 "extra": {
@@ -2219,20 +2342,20 @@
2219 ], 2342 ],
2220 "description": "Symfony Finder Component", 2343 "description": "Symfony Finder Component",
2221 "homepage": "https://symfony.com", 2344 "homepage": "https://symfony.com",
2222 "time": "2017-06-01T21:01:25+00:00" 2345 "time": "2017-10-02T06:42:24+00:00"
2223 }, 2346 },
2224 { 2347 {
2225 "name": "symfony/polyfill-mbstring", 2348 "name": "symfony/polyfill-mbstring",
2226 "version": "v1.4.0", 2349 "version": "v1.6.0",
2227 "source": { 2350 "source": {
2228 "type": "git", 2351 "type": "git",
2229 "url": "https://github.com/symfony/polyfill-mbstring.git", 2352 "url": "https://github.com/symfony/polyfill-mbstring.git",
2230 "reference": "f29dca382a6485c3cbe6379f0c61230167681937" 2353 "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296"
2231 }, 2354 },
2232 "dist": { 2355 "dist": {
2233 "type": "zip", 2356 "type": "zip",
2234 "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f29dca382a6485c3cbe6379f0c61230167681937", 2357 "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
2235 "reference": "f29dca382a6485c3cbe6379f0c61230167681937", 2358 "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
2236 "shasum": "" 2359 "shasum": ""
2237 }, 2360 },
2238 "require": { 2361 "require": {
@@ -2244,7 +2367,7 @@
2244 "type": "library", 2367 "type": "library",
2245 "extra": { 2368 "extra": {
2246 "branch-alias": { 2369 "branch-alias": {
2247 "dev-master": "1.4-dev" 2370 "dev-master": "1.6-dev"
2248 } 2371 }
2249 }, 2372 },
2250 "autoload": { 2373 "autoload": {
@@ -2278,24 +2401,24 @@
2278 "portable", 2401 "portable",
2279 "shim" 2402 "shim"
2280 ], 2403 ],
2281 "time": "2017-06-09T14:24:12+00:00" 2404 "time": "2017-10-11T12:05:26+00:00"
2282 }, 2405 },
2283 { 2406 {
2284 "name": "symfony/yaml", 2407 "name": "symfony/yaml",
2285 "version": "v3.3.6", 2408 "version": "v3.3.10",
2286 "source": { 2409 "source": {
2287 "type": "git", 2410 "type": "git",
2288 "url": "https://github.com/symfony/yaml.git", 2411 "url": "https://github.com/symfony/yaml.git",
2289 "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed" 2412 "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46"
2290 }, 2413 },
2291 "dist": { 2414 "dist": {
2292 "type": "zip", 2415 "type": "zip",
2293 "url": "https://api.github.com/repos/symfony/yaml/zipball/ddc23324e6cfe066f3dd34a37ff494fa80b617ed", 2416 "url": "https://api.github.com/repos/symfony/yaml/zipball/8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
2294 "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed", 2417 "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
2295 "shasum": "" 2418 "shasum": ""
2296 }, 2419 },
2297 "require": { 2420 "require": {
2298 "php": ">=5.5.9" 2421 "php": "^5.5.9|>=7.0.8"
2299 }, 2422 },
2300 "require-dev": { 2423 "require-dev": {
2301 "symfony/console": "~2.8|~3.0" 2424 "symfony/console": "~2.8|~3.0"
@@ -2333,7 +2456,7 @@
2333 ], 2456 ],
2334 "description": "Symfony Yaml Component", 2457 "description": "Symfony Yaml Component",
2335 "homepage": "https://symfony.com", 2458 "homepage": "https://symfony.com",
2336 "time": "2017-07-23T12:43:26+00:00" 2459 "time": "2017-10-05T14:43:42+00:00"
2337 }, 2460 },
2338 { 2461 {
2339 "name": "theseer/fdomdocument", 2462 "name": "theseer/fdomdocument",
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
36$ mkdir -p /path/to/shaarli && cd /path/to/shaarli/ 36$ mkdir -p /path/to/shaarli && cd /path/to/shaarli/
37$ git clone -b v0.9 https://github.com/shaarli/Shaarli.git . 37$ git clone -b v0.9 https://github.com/shaarli/Shaarli.git .
38$ composer install --no-dev --prefer-dist 38$ composer install --no-dev --prefer-dist
39$ make translate
39``` 40```
40 41
41## Stable version 42## Stable version
@@ -83,13 +84,14 @@ $ git clone https://github.com/shaarli/Shaarli.git -b master /path/to/shaarli/
83# install/update third-party dependencies 84# install/update third-party dependencies
84$ cd /path/to/shaarli 85$ cd /path/to/shaarli
85$ composer install --no-dev --prefer-dist 86$ composer install --no-dev --prefer-dist
87$ make translate
86``` 88```
87 89
88## Finish Installation 90## Finish Installation
89 91
90Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser. 92Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser.
91 93
92![install screenshot](http://i.imgur.com/wuMpDSN.png) 94![install screenshot](images/install-shaarli.png)
93 95
94Setup your Shaarli installation, and it's ready to use! 96Setup your Shaarli installation, and it's ready to use!
95 97
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
39[`php-gd`](http://php.net/manual/en/book.image.php) | optional | thumbnail resizing 39[`php-gd`](http://php.net/manual/en/book.image.php) | optional | thumbnail resizing
40[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`) 40[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`)
41[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way 41[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way
42[`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_
81- **page_cache**: Shaarli's internal cache directory. 81- **page_cache**: Shaarli's internal cache directory.
82- **ban_file**: Banned IP file path. 82- **ban_file**: Banned IP file path.
83 83
84### Translation
85
86- **language**: translation language (also see [Translations](Translations))
87 - **auto** (default): The translation language is chosen from the browser locale.
88 It means that the language can be different for 2 different visitors depending on their locale.
89 - **en**: Use the English translation.
90 - **fr**: Use the French translation.
91- **mode**:
92 - **auto** or **php** (default): Use the PHP implementation of gettext (slower)
93 - **gettext**: Use PHP builtin gettext extension
94 (faster, but requires `php-gettext` to be installed and to reload the web server on update)
95- **extension**: Translation extensions for custom themes or plugins.
96Must be an associative array: `translation domain => translation path`.
97
84### Updates 98### Updates
85 99
86- **check_updates**: Enable or disable update check to the git repository. 100- **check_updates**: Enable or disable update check to the git repository.
@@ -211,6 +225,13 @@ _These settings should not be edited_
211 "plugins": { 225 "plugins": {
212 "WALLABAG_URL": "http://demo.wallabag.org", 226 "WALLABAG_URL": "http://demo.wallabag.org",
213 "WALLABAG_VERSION": "1" 227 "WALLABAG_VERSION": "1"
228 },
229 "translation": {
230 "language": "fr",
231 "mode": "php",
232 "extensions": {
233 "demo": "plugins/demo_plugin/languages/"
234 }
214 } 235 }
215} ?> 236} ?>
216``` 237```
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 @@
1## Translations
2
3Shaarli supports [gettext](https://www.gnu.org/software/gettext/manual/gettext.html) translations
4since `>= v0.9.2`.
5
6Note that only the `default` theme supports translations.
7
8### Contributing
9
10We encourage the community to contribute to Shaarli's translation either by improving existing
11translations or submitting a new language.
12
13Contributing to the translation does not require development skill.
14
15Please submit a pull request with the `.po` file updated/created. Note that the compiled file (`.mo`)
16is not stored on the repository, and is generated during the release process.
17
18### How to
19
20First, install [Poedit](https://poedit.net/) tool.
21
22Poedit will extract strings to translate from the PHP source code.
23
24**Important**: due to the usage of a template engine, it's important to generate PHP cache files to extract
25every translatable string.
26
27You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended)
28or visit every template page in your browser to generate cache files, while logged in.
29
30Here is a list :
31
32```
33http://<replace_domain>/
34http://<replace_domain>/?nonope
35http://<replace_domain>/?do=addlink
36http://<replace_domain>/?do=changepasswd
37http://<replace_domain>/?do=changetag
38http://<replace_domain>/?do=configure
39http://<replace_domain>/?do=tools
40http://<replace_domain>/?do=daily
41http://<replace_domain>/?post
42http://<replace_domain>/?do=export
43http://<replace_domain>/?do=import
44http://<replace_domain>/?do=login
45http://<replace_domain>/?do=picwall
46http://<replace_domain>/?do=pluginadmin
47http://<replace_domain>/?do=tagcloud
48http://<replace_domain>/?do=taglist
49```
50
51#### Improve existing translation
52
53In Poedit, click on "Edit a Translation", and from Shaarli's directory open
54`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
55
56The existing list of translatable strings should have been loaded, then click on the "Update" button.
57
58You can start editing the translation.
59
60![poedit-screenshot](images/poedit-1.jpg)
61
62Save when you're done, then you can submit a pull request containing the updated `shaarli.po`.
63
64#### Add a new language
65
66Open Poedit and select "Create New Translation", then from Shaarli's directory open
67`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
68
69Then select the language you want to create.
70
71Click on `File > Save as...`, and save your file in `<shaarli directory>/inc/language/<new language>/LC_MESSAGES/shaarli.po`.
72`<new language>` here should be the language code respecting the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-2)
73format in lowercase (e.g. `de` for German).
74
75Then click on the "Update" button, and you can start to translate every available string.
76
77Save when you're done, then you can submit a pull request containing the new `shaarli.po`.
78
79### Extend Shaarli's translation
80
81If you're writing a custom theme, or a non official plugin, you might want to use the translation system,
82but you won't be able to able to override Shaarli's translation.
83
84However, you can add your own translation domain which extends the main translation list.
85
86> Note that you can find a live example of translation extension in the `demo_plugin`.
87
88First, create your translation files tree directory:
89
90```
91<your_module>/languages/<ISO 3166-1 alpha-2 language code>/LC_MESSAGES/
92```
93
94Your `.po` files must be named like your domain. E.g. if your translation domain is `my_theme`, then your file will be
95`my_theme.po`.
96
97Users have to register your extension in their configuration with the parameter
98`translation.extensions.<domain>: <translation files path>`.
99
100Example:
101
102```php
103if (! $conf->exists('translation.extensions.my_theme')) {
104 $conf->set('translation.extensions.my_theme', '<your_module>/languages/');
105 $conf->write(true);
106}
107```
108
109> Note that the page needs to be reloaded after the registration.
110
111It is then recommended to create a custom translation function which will call the `t()` function with your domain.
112For example :
113
114```php
115function my_theme_t($text, $nText = '', $nb = 1)
116{
117 return t($text, $nText, $nb, 'my_theme'); // the last parameter is your translation domain.
118}
119```
120
121All strings which can be translated should be processed through your function:
122
123```php
124my_theme_t('Comment');
125my_theme_t('Comment', 'Comments', 2);
126```
127
128Or in templates:
129
130```php
131{'Comment'|my_theme_t}
132{function="my_theme_t('Comment', 'Comments', 2)"}
133```
134
135> Note than in template, you need to visit your page at least once to generate a cache file.
136
137When you're done, open Poedit and load translation strings from sources:
138
139 1. `File > New`
140 2. Choose your language
141 3. Save your `PO` file in `<your_module>/languages/<language code>/LC_MESSAGES/my_theme.po`.
142 4. Go to `Catalog > Properties...`
143 5. Fill the `Translation Properties` tab
144 6. Add your source path in the `Sources Paths` tab
145 7. In the `Sources Keywords` tab uncheck "Also use default keywords" and add the following lines:
146
147```
148my_theme_t
149my_theme_t:1,2
150```
151
152Click 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
39 39
40Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory! 40Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory!
41 41
42After 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). 42If you use translations in gettext mode - meaning you manually changed the default mode -,
43reload your web server.
44
45After 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).
43 46
44## Upgrading with Git 47## Upgrading with Git
45 48
@@ -72,6 +75,14 @@ Updating dependencies
72 Downloading: 100% 75 Downloading: 100%
73``` 76```
74 77
78Shaarli >= `v0.9.2` supports translations:
79
80```bash
81$ make translate
82```
83
84If you use translations in gettext mode, reload your web server.
85
75### Migrating and upgrading from Sebsauvage's repository 86### Migrating and upgrading from Sebsauvage's repository
76 87
77If 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. 88If 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
151 Downloading: 100% 162 Downloading: 100%
152``` 163```
153 164
165Shaarli >= `v0.9.2` supports translations:
166
167```bash
168$ make translate
169```
170
171If you use translations in gettext mode, reload your web server.
172
154Optionally, you can delete information related to the legacy version: 173Optionally, you can delete information related to the legacy version:
155 174
156```bash 175```bash
diff --git a/doc/md/images/install-shaarli.png b/doc/md/images/install-shaarli.png
new file mode 100644
index 00000000..7ae33816
--- /dev/null
+++ b/doc/md/images/install-shaarli.png
Binary files differ
diff --git a/doc/md/images/poedit-1.jpg b/doc/md/images/poedit-1.jpg
new file mode 100644
index 00000000..673ae6d6
--- /dev/null
+++ b/doc/md/images/poedit-1.jpg
Binary files differ
diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po
new file mode 100644
index 00000000..6b2de950
--- /dev/null
+++ b/inc/languages/fr/LC_MESSAGES/shaarli.po
@@ -0,0 +1,1366 @@
1msgid ""
2msgstr ""
3"Project-Id-Version: Shaarli\n"
4"POT-Creation-Date: 2017-10-22 13:13+0200\n"
5"PO-Revision-Date: 2017-10-22 13:14+0200\n"
6"Last-Translator: \n"
7"Language-Team: Shaarli\n"
8"Language: fr_FR\n"
9"MIME-Version: 1.0\n"
10"Content-Type: text/plain; charset=UTF-8\n"
11"Content-Transfer-Encoding: 8bit\n"
12"X-Generator: Poedit 2.0.4\n"
13"X-Poedit-Basepath: ../../../..\n"
14"Plural-Forms: nplurals=2; plural=(n > 1);\n"
15"X-Poedit-SourceCharset: UTF-8\n"
16"X-Poedit-KeywordsList: t:1,2;t\n"
17"X-Poedit-SearchPath-0: .\n"
18
19#: application/ApplicationUtils.php:153
20#, php-format
21msgid ""
22"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
23"cannot run. Your PHP version has known security vulnerabilities and should "
24"be updated as soon as possible."
25msgstr ""
26"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne "
27"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
28"connues et devrait être mise à jour au plus tôt."
29
30#: application/ApplicationUtils.php:183 application/ApplicationUtils.php:195
31msgid "directory is not readable"
32msgstr "le répertoire n'est pas accessible en lecture"
33
34#: application/ApplicationUtils.php:198
35msgid "directory is not writable"
36msgstr "le répertoire n'est pas accessible en écriture"
37
38#: application/ApplicationUtils.php:216
39msgid "file is not readable"
40msgstr "le fichier n'est pas accessible en lecture"
41
42#: application/ApplicationUtils.php:219
43msgid "file is not writable"
44msgstr "le fichier n'est pas accessible en écriture"
45
46#: application/Cache.php:16
47#, php-format
48msgid "Cannot purge %s: no directory"
49msgstr "Impossible de purger %s: le répertoire n'existe pas"
50
51#: application/FeedBuilder.php:151
52msgid "Direct link"
53msgstr "Liens directs"
54
55#: application/FeedBuilder.php:153
56#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
57#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:178
58msgid "Permalink"
59msgstr "Permalien"
60
61#: application/History.php:174
62msgid "History file isn't readable or writable"
63msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
64
65#: application/History.php:185
66msgid "Could not parse history file"
67msgstr "Format incorrect pour le fichier d'historique"
68
69#: application/Languages.php:159
70msgid "Automatic"
71msgstr "Automatique"
72
73#: application/Languages.php:160
74msgid "English"
75msgstr "Anglais"
76
77#: application/Languages.php:161
78msgid "French"
79msgstr "Français"
80
81#: application/LinkDB.php:136
82msgid "You are not authorized to add a link."
83msgstr "Vous n'êtes pas autorisé à ajouter un lien."
84
85#: application/LinkDB.php:139
86msgid "Internal Error: A link should always have an id and URL."
87msgstr "Erreur interne : un lien devrait toujours avoir un ID et une URL."
88
89#: application/LinkDB.php:142
90msgid "You must specify an integer as a key."
91msgstr "Vous devez utiliser un entier comme clé."
92
93#: application/LinkDB.php:145
94msgid "Array offset and link ID must be equal."
95msgstr "La clé du tableau et l'ID du lien doivent être égaux."
96
97#: application/LinkDB.php:251
98#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
99#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
100#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14
101#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
102msgid ""
103"The personal, minimalist, super-fast, database free, bookmarking service"
104msgstr ""
105"Le gestionnaire de marque-page personnel, minimaliste, et sans base de "
106"données"
107
108#: application/LinkDB.php:253
109msgid ""
110"Welcome to Shaarli! This is your first public bookmark. To edit or delete "
111"me, you must first login.\n"
112"\n"
113"To learn how to use Shaarli, consult the link \"Documentation\" at the "
114"bottom of this page.\n"
115"\n"
116"You use the community supported version of the original Shaarli project, by "
117"Sebastien Sauvage."
118msgstr ""
119"Bienvenue sur Shaarli ! Ceci est votre premier marque-page public. Pour me "
120"modifier ou me supprimer, vous devez d'abord vous connecter.\n"
121"\n"
122"Pour apprendre comment utiliser Shaarli, consultez le lien « Documentation » "
123"en bas de page.\n"
124"\n"
125"Vous utilisez la version supportée par la communauté du projet original "
126"Shaarli, de Sébastien Sauvage."
127
128#: application/LinkDB.php:267
129msgid "My secret stuff... - Pastebin.com"
130msgstr "Mes trucs secrets... - Pastebin.com"
131
132#: application/LinkDB.php:269
133msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
134msgstr ""
135"Pssst ! Je suis un lien privé que VOUS êtes le seul à voir. Vous pouvez me "
136"supprimer aussi."
137
138#: application/LinkFilter.php:452
139msgid "The link you are trying to reach does not exist or has been deleted."
140msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé."
141
142#: application/NetscapeBookmarkUtils.php:35
143msgid "Invalid export selection:"
144msgstr "Sélection d'export invalide :"
145
146#: application/NetscapeBookmarkUtils.php:81
147#, php-format
148msgid "File %s (%d bytes) "
149msgstr "Le fichier %s (%d octets) "
150
151#: application/NetscapeBookmarkUtils.php:83
152msgid "has an unknown file format. Nothing was imported."
153msgstr "a un format inconnu. Rien n'a été importé."
154
155#: application/NetscapeBookmarkUtils.php:86
156#, php-format
157msgid ""
158"was successfully processed in %d seconds: %d links imported, %d links "
159"overwritten, %d links skipped."
160msgstr ""
161"a été importé avec succès en %d secondes : %d liens importés, %d liens "
162"écrasés, %d liens ignorés."
163
164#: application/PageBuilder.php:165
165msgid "The page you are trying to reach does not exist or has been deleted."
166msgstr "La page que vous essayez de consulter n'existe pas ou a été supprimée."
167
168#: application/PageBuilder.php:167
169msgid "404 Not Found"
170msgstr "404 Introuvable"
171
172#: application/PluginManager.php:243
173#, php-format
174msgid "Plugin \"%s\" files not found."
175msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
176
177#: application/Updater.php:76
178msgid "Couldn't retrieve Updater class methods."
179msgstr "Impossible de récupérer les méthodes de la classe Updater."
180
181#: application/Updater.php:485
182msgid "An error occurred while running the update "
183msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
184
185#: application/Updater.php:525
186msgid "Updates file path is not set, can't write updates."
187msgstr ""
188"Le chemin vers le fichier de mise à jour n'est pas défini, impossible "
189"d'écrire les mises à jour."
190
191#: application/Updater.php:530
192msgid "Unable to write updates in "
193msgstr "Impossible d'écrire les mises à jour dans "
194
195#: application/Utils.php:406 tests/UtilsTest.php:398
196msgid "Setting not set"
197msgstr "Paramètre non défini"
198
199#: application/Utils.php:413 tests/UtilsTest.php:396 tests/UtilsTest.php:397
200msgid "Unlimited"
201msgstr "Illimité"
202
203#: application/Utils.php:416 tests/UtilsTest.php:393 tests/UtilsTest.php:394
204#: tests/UtilsTest.php:408
205msgid "B"
206msgstr "o"
207
208#: application/Utils.php:416 tests/UtilsTest.php:387 tests/UtilsTest.php:388
209#: tests/UtilsTest.php:395
210msgid "kiB"
211msgstr "ko"
212
213#: application/Utils.php:416 tests/UtilsTest.php:389 tests/UtilsTest.php:390
214#: tests/UtilsTest.php:406 tests/UtilsTest.php:407
215msgid "MiB"
216msgstr "Mo"
217
218#: application/Utils.php:416 tests/UtilsTest.php:391 tests/UtilsTest.php:392
219msgid "GiB"
220msgstr "Go"
221
222#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:121
223msgid ""
224"Shaarli could not create the config file. Please make sure Shaarli has the "
225"right to write in the folder is it installed in."
226msgstr ""
227"Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que "
228"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé."
229
230#: application/config/ConfigManager.php:135
231msgid "Invalid setting key parameter. String expected, got: "
232msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : "
233
234#: application/config/exception/MissingFieldConfigException.php:21
235#, php-format
236msgid "Configuration value is required for %s"
237msgstr "Le paramètre %s est obligatoire"
238
239#: application/config/exception/PluginConfigOrderException.php:15
240msgid "An error occurred while trying to save plugins loading order."
241msgstr ""
242"Une erreur s'est produite lors de la sauvegarde de l'ordre des extensions."
243
244#: application/config/exception/UnauthorizedConfigException.php:16
245msgid "You are not authorized to alter config."
246msgstr "Vous n'êtes pas autorisé à modifier la configuration."
247
248#: application/exceptions/IOException.php:19
249msgid "Error accessing"
250msgstr "Une erreur s'est produite en accédant à"
251
252#: index.php:133
253msgid "Shared links on "
254msgstr "Liens partagés sur "
255
256#: index.php:155
257msgid "Insufficient permissions:"
258msgstr "Permissions insuffisantes :"
259
260#: index.php:382
261msgid "I said: NO. You are banned for the moment. Go away."
262msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard."
263
264#: index.php:447
265msgid "Wrong login/password."
266msgstr "Nom d'utilisateur ou mot de passe incorrects."
267
268#: index.php:1107
269msgid "You are not supposed to change a password on an Open Shaarli."
270msgstr ""
271"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert."
272
273#: index.php:1112 index.php:1153 index.php:1229 index.php:1259 index.php:1359
274msgid "Wrong token."
275msgstr "Jeton invalide."
276
277#: index.php:1117
278msgid "The old password is not correct."
279msgstr "L'ancien mot de passe est incorrect."
280
281#: index.php:1137
282msgid "Your password has been changed"
283msgstr "Votre mot de passe a été modifié"
284
285#: index.php:1190
286msgid "Configuration was saved."
287msgstr "La configuration a été sauvegardé."
288
289#: index.php:1241
290#, php-format
291msgid "The tag was removed from %d link."
292msgid_plural "The tag was removed from %d links."
293msgstr[0] "Le tag a été supprimé de %d lien."
294msgstr[1] "Le tag a été supprimé de %d liens."
295
296#: index.php:1242
297#, php-format
298msgid "The tag was renamed in %d link."
299msgid_plural "The tag was renamed in %d links."
300msgstr[0] "Le tag a été renommé dans %d lien."
301msgstr[1] "Le tag a été renommé dans %d liens."
302
303#: index.php:1458
304msgid "Note: "
305msgstr "Note : "
306
307#: index.php:1567
308#, php-format
309msgid ""
310"The file you are trying to upload is probably bigger than what this "
311"webserver can accept (%s). Please upload in smaller chunks."
312msgstr ""
313"Le fichier que vous essayer d'envoyer est probablement plus lourd que ce que "
314"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
315"légères."
316
317#: index.php:1983
318#, php-format
319msgid ""
320"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
321"variable \"session.save_path\" is set correctly in your PHP config, and that "
322"you have write access to it.<br>It currently points to %s.<br>On some "
323"browsers, accessing your server via a hostname like 'localhost' or any "
324"custom hostname without a dot causes cookie storage to fail. We recommend "
325"accessing your server via it's IP address or Fully Qualified Domain Name.<br>"
326msgstr ""
327"<pre>Les sesssions ne semble pas fonctionner sur ce serveur.<br>Assurez vous "
328"que la variable « session.save_path » est correctement définie dans votre "
329"fichier de configuration PHP, et que vous y avez les droits d'écriture."
330"<br>Ce paramètre pointe actuellement sur %s.<br>Sur certains navigateurs, "
331"accéder à votre serveur depuis un nom d'hôte comme « localhost » ou autre "
332"nom personnalisé sans point '.' entraine l'échec de la sauvegarde des "
333"cookies. Nous vous recommandons d'accéder à votre serveur depuis son adresse "
334"IP ou un <em>Fully Qualified Domain Name</em>.<br>"
335
336#: index.php:1993
337msgid "Click to try again."
338msgstr "Cliquer ici pour réessayer."
339
340#: plugins/addlink_toolbar/addlink_toolbar.php:29
341msgid "URI"
342msgstr "URI"
343
344#: plugins/addlink_toolbar/addlink_toolbar.php:33
345#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
346msgid "Add link"
347msgstr "Shaare"
348
349#: plugins/addlink_toolbar/addlink_toolbar.php:50
350msgid "Adds the addlink input on the linklist page."
351msgstr "Ajout le formulaire d'ajout de liens sur la page principale."
352
353#: plugins/archiveorg/archiveorg.php:23
354msgid "View on archive.org"
355msgstr "Voir sur archive.org"
356
357#: plugins/archiveorg/archiveorg.php:36
358msgid "For each link, add an Archive.org icon."
359msgstr "Pour chaque lien, ajoute une icône pour Archive.org."
360
361#: plugins/demo_plugin/demo_plugin.php:469
362msgid ""
363"A demo plugin covering all use cases for template designers and plugin "
364"developers."
365msgstr ""
366"Une extension de démonstration couvrant tous les cas d'utilisation pour les "
367"designers et les développeurs."
368
369#: plugins/isso/isso.php:20
370msgid ""
371"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin "
372"administration page."
373msgstr ""
374"Erreur de l'extension Isso : Merci de définir le paramètre « ISSO_SERVER » "
375"dans la page d'administration des extensions."
376
377#: plugins/isso/isso.php:63
378msgid "Let visitor comment your shaares on permalinks with Isso."
379msgstr ""
380"Permet aux visiteurs de commenter vos shaares sur les permaliens avec Isso."
381
382#: plugins/isso/isso.php:64
383msgid "Isso server URL (without 'http://')"
384msgstr "URL du serveur Isso (sans 'http://')"
385
386#: plugins/markdown/markdown.php:159
387msgid "Description will be rendered with"
388msgstr "La description sera générée avec"
389
390#: plugins/markdown/markdown.php:160
391msgid "Markdown syntax documentation"
392msgstr "Documentation sur la syntaxe Markdown"
393
394#: plugins/markdown/markdown.php:161
395msgid "Markdown syntax"
396msgstr "la syntaxe Markdown"
397
398#: plugins/markdown/markdown.php:340
399msgid ""
400"Render shaare description with Markdown syntax.<br><strong>Warning</"
401"strong>:\n"
402"If your shaared descriptions contained HTML tags before enabling the "
403"markdown plugin,\n"
404"enabling it might break your page.\n"
405"See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
406"markdown#html-rendering\">README</a>."
407msgstr ""
408"Utilise la syntaxe Markdown pour la description des liens."
409"<br><strong>Attention</strong> :\n"
410"Si vous aviez des descriptions contenant du HTML avant d'activer cette "
411"extension,\n"
412"l'activer pourrait déformer vos pages.\n"
413"Voir le <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
414"markdown#html-rendering\">README</a>."
415
416#: plugins/piwik/piwik.php:21
417msgid ""
418"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
419"administration page."
420msgstr ""
421"Erreur de l'extension Piwik : Merci de définir les paramètres PIWIK_URL et "
422"PIWIK_SITEID dans la page d'administration des extensions."
423
424#: plugins/piwik/piwik.php:70
425msgid "A plugin that adds Piwik tracking code to Shaarli pages."
426msgstr "Ajoute le code de traçage de Piwik sur les pages de Shaarli."
427
428#: plugins/piwik/piwik.php:71
429msgid "Piwik URL"
430msgstr "URL de Piwik"
431
432#: plugins/piwik/piwik.php:72
433msgid "Piwik site ID"
434msgstr "Site ID de Piwik"
435
436#: plugins/playvideos/playvideos.php:22
437msgid "Video player"
438msgstr "Lecteur vidéo"
439
440#: plugins/playvideos/playvideos.php:25
441msgid "Play Videos"
442msgstr "Jouer les vidéos"
443
444#: plugins/playvideos/playvideos.php:56
445msgid "Add a button in the toolbar allowing to watch all videos."
446msgstr ""
447"Ajoute un bouton dans la barre de menu pour regarder toutes les vidéos."
448
449#: plugins/playvideos/youtube_playlist.js:214
450msgid "plugins/playvideos/jquery-1.11.2.min.js"
451msgstr ""
452
453#: plugins/pubsubhubbub/pubsubhubbub.php:69
454#, php-format
455msgid "Could not publish to PubSubHubbub: %s"
456msgstr "Impossible de publier vers PubSubHubbub : %s"
457
458#: plugins/pubsubhubbub/pubsubhubbub.php:95
459#, php-format
460msgid "Could not post to %s"
461msgstr "Impossible de publier vers %s"
462
463#: plugins/pubsubhubbub/pubsubhubbub.php:99
464#, php-format
465msgid "Bad response from the hub %s"
466msgstr "Mauvaise réponse du hub %s"
467
468#: plugins/pubsubhubbub/pubsubhubbub.php:110
469msgid "Enable PubSubHubbub feed publishing."
470msgstr "Active la publication de flux vers PubSubHubbub."
471
472#: plugins/qrcode/qrcode.php:69 plugins/wallabag/wallabag.php:68
473msgid "For each link, add a QRCode icon."
474msgstr "Pour chaque liens, ajouter une icône de QRCode."
475
476#: plugins/wallabag/wallabag.php:21
477msgid ""
478"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the "
479"plugin administration page."
480msgstr ""
481"Erreur de l'extension Wallabag : Merci de définir le paramètre « "
482"WALLABAG_URL » dans la page d'administration des extensions."
483
484#: plugins/wallabag/wallabag.php:47
485msgid "Save to wallabag"
486msgstr "Sauvegarder dans Wallabag"
487
488#: plugins/wallabag/wallabag.php:69
489msgid "Wallabag API URL"
490msgstr "URL de l'API Wallabag"
491
492#: plugins/wallabag/wallabag.php:70
493msgid "Wallabag API version (1 or 2)"
494msgstr "Version de l'API Wallabag (1 ou 2)"
495
496#: tests/LanguagesTest.php:188 tests/LanguagesTest.php:201
497#: tests/languages/fr/LanguagesFrTest.php:160
498#: tests/languages/fr/LanguagesFrTest.php:173
499#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
500#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:81
501msgid "Search"
502msgid_plural "Search"
503msgstr[0] "Rechercher"
504msgstr[1] "Rechercher"
505
506#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
507msgid "Sorry, nothing to see here."
508msgstr "Désolé, il y a rien à voir ici."
509
510#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
511msgid "Shaare a new link"
512msgstr "Partager un nouveau lien"
513
514#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
515msgid "URL or leave empty to post a note"
516msgstr "URL ou laisser vide pour créer une note"
517
518#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
519#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
520msgid "Change password"
521msgstr "Modification du mot de passe"
522
523#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
524msgid "Current password"
525msgstr "Mot de passe actuel"
526
527#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
528msgid "New password"
529msgstr "Nouveau mot de passe"
530
531#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
532msgid "Change"
533msgstr "Changer"
534
535#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
536#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
537msgid "Manage tags"
538msgstr "Gérer les tags"
539
540#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
541#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
542msgid "Tag"
543msgstr "Tag"
544
545#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
546msgid "New name"
547msgstr "Nouveau nom"
548
549#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
550msgid "Case sensitive"
551msgstr "Sensible à la casse"
552
553#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
554msgid "Rename"
555msgstr "Renommer"
556
557#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
558#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
559#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:172
560msgid "Delete"
561msgstr "Supprimer"
562
563#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
564msgid "You can also edit tags in the"
565msgstr "Vous pouvez aussi modifier les tags dans la"
566
567#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
568msgid "tag list"
569msgstr "liste des tags"
570
571#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
572msgid "Configure"
573msgstr "Configurer"
574
575#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
576msgid "title"
577msgstr "titre"
578
579#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
580msgid "Home link"
581msgstr "Lien vers l'accueil"
582
583#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
584msgid "Default value"
585msgstr "Valeur par défaut"
586
587#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
588msgid "Theme"
589msgstr "Thème"
590
591#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
592#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
593msgid "Language"
594msgstr "Langue"
595
596#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116
597#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
598msgid "Timezone"
599msgstr "Fuseau horaire"
600
601#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
602#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
603msgid "Continent"
604msgstr "Continent"
605
606#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
607#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
608msgid "City"
609msgstr "Ville"
610
611#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:163
612msgid "Redirector"
613msgstr "Redirecteur"
614
615#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164
616msgid "e. g."
617msgstr "ex :"
618
619#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164
620msgid "will mask the HTTP_REFERER"
621msgstr "masque le HTTP_REFERER"
622
623#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
624msgid "Disable session cookie hijacking protection"
625msgstr "Désactiver la protection contre le détournement de cookies"
626
627#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
628msgid "Check this if you get disconnected or if your IP address changes often"
629msgstr ""
630"Cocher cette case si vous êtes souvent déconnecté ou si votre adresse IP "
631"change souvent"
632
633#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:198
634msgid "Private links by default"
635msgstr "Liens privés par défaut"
636
637#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
638msgid "All new links are private by default"
639msgstr "Tous les nouveaux liens sont privés par défaut"
640
641#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:214
642msgid "RSS direct links"
643msgstr "Liens directs dans le flux RSS"
644
645#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215
646msgid "Check this to use direct URL instead of permalink in feeds"
647msgstr ""
648"Cocher cette case pour utiliser des liens directs au lieu des permaliens "
649"dans le flux RSS"
650
651#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:230
652msgid "Hide public links"
653msgstr "Cacher les liens publics"
654
655#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:231
656msgid "Do not show any links if the user is not logged in"
657msgstr "N'afficher aucun lien sans être connecté"
658
659#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:246
660#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
661msgid "Check updates"
662msgstr "Vérifier les mises à jour"
663
664#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247
665#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
666msgid "Notify me when a new release is ready"
667msgstr "Me notifier lorsqu'une nouvelle version est disponible"
668
669#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:262
670#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
671msgid "Enable REST API"
672msgstr "Activer l'API REST"
673
674#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263
675#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
676msgid "Allow third party software to use Shaarli such as mobile application"
677msgstr ""
678"Permets aux applications tierces d'utiliser Shaarli, par exemple les "
679"applications mobiles"
680
681#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:278
682msgid "API secret"
683msgstr "Clé d'API secrète"
684
685#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:289
686#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
687#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
688#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:192
689msgid "Save"
690msgstr "Enregistrer"
691
692#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
693msgid "The Daily Shaarli"
694msgstr "Le Quotidien Shaarli"
695
696#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
697msgid "1 RSS entry per day"
698msgstr "1 entrée RSS par jour"
699
700#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
701msgid "Previous day"
702msgstr "Jour précédent"
703
704#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
705msgid "All links of one day in a single page."
706msgstr "Tous les liens d'un jour sur une page."
707
708#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
709msgid "Next day"
710msgstr "Jour suivant"
711
712#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
713#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
714msgid "Edit"
715msgstr "Modifier"
716
717#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
718#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
719#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26
720msgid "Shaare"
721msgstr "Shaare"
722
723#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
724msgid "Created:"
725msgstr "Création :"
726
727#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
728msgid "URL"
729msgstr "URL"
730
731#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
732msgid "Title"
733msgstr "Titre"
734
735#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
736#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
737#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
738#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
739#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
740msgid "Description"
741msgstr "Description"
742
743#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
744msgid "Tags"
745msgstr "Tags"
746
747#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59
748#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
749#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
750msgid "Private"
751msgstr "Privé"
752
753#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
754msgid "Apply Changes"
755msgstr "Appliquer les changements"
756
757#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
758msgid "Export Database"
759msgstr "Exporter les données"
760
761#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
762msgid "Selection"
763msgstr "Choisir"
764
765#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
766msgid "All"
767msgstr "Tous"
768
769#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
770msgid "Public"
771msgstr "Publics"
772
773#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
774msgid "Prepend note permalinks with this Shaarli instance's URL"
775msgstr "Préfixer les liens de notes avec l'URL de l'instance de Shaarli"
776
777#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
778msgid "Useful to import bookmarks in a web browser"
779msgstr "Utile pour importer les marques-pages dans un navigateur"
780
781#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
782msgid "Export"
783msgstr "Exporter"
784
785#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
786msgid "Import Database"
787msgstr "Importer des données"
788
789#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
790msgid "Maximum size allowed:"
791msgstr "Taille maximum autorisée :"
792
793#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
794msgid "Visibility"
795msgstr "Visibilité"
796
797#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
798msgid "Use values from the imported file, default to public"
799msgstr ""
800"Utiliser les valeurs présentes dans le fichier d'import, public par défaut"
801
802#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
803msgid "Import all bookmarks as private"
804msgstr "Importer tous les liens comme privés"
805
806#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
807msgid "Import all bookmarks as public"
808msgstr "Importer tous les liens comme publics"
809
810#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57
811msgid "Overwrite existing bookmarks"
812msgstr "Remplacer les liens existants"
813
814#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
815msgid "Duplicates based on URL"
816msgstr "Les doublons s'appuient sur les URL"
817
818#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
819msgid "Add default tags"
820msgstr "Ajouter des tags par défaut"
821
822#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
823msgid "Import"
824msgstr "Importer"
825
826#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
827msgid "Install Shaarli"
828msgstr "Installation de Shaarli"
829
830#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
831msgid "It looks like it's the first time you run Shaarli. Please configure it."
832msgstr ""
833"Il semblerait que ça soit la première fois que vous lancez Shaarli. Merci de "
834"le configurer."
835
836#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
837#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
838#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
839#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
840msgid "Username"
841msgstr "Nom d'utilisateur"
842
843#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
844#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
845#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148
846#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:148
847msgid "Password"
848msgstr "Mot de passe"
849
850#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
851msgid "Shaarli title"
852msgstr "Titre du Shaarli"
853
854#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
855msgid "My links"
856msgstr "Mes liens"
857
858#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
859msgid "Install"
860msgstr "Installer"
861
862#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
863#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80
864msgid "shaare"
865msgid_plural "shaares"
866msgstr[0] "shaare"
867msgstr[1] "shaares"
868
869#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
870#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
871msgid "private link"
872msgid_plural "private links"
873msgstr[0] "lien privé"
874msgstr[1] "liens privés"
875
876#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
877#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
878#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:117
879msgid "Search text"
880msgstr "Recherche texte"
881
882#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
883#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
884#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:124
885#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
886#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
887#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
888#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
889msgid "Filter by tag"
890msgstr "Filtrer par tag"
891
892#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111
893msgid "Nothing found."
894msgstr "Aucun résultat."
895
896#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:119
897#, php-format
898msgid "%s result"
899msgid_plural "%s results"
900msgstr[0] "%s résultat"
901msgstr[1] "%s résultats"
902
903#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
904msgid "for"
905msgstr "pour"
906
907#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130
908msgid "tagged"
909msgstr "taggé"
910
911#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
912msgid "Remove tag"
913msgstr "Retirer le tag"
914
915#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143
916msgid "with status"
917msgstr "avec le statut"
918
919#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154
920msgid "without any tag"
921msgstr "sans tag"
922
923#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:174
924#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
925#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42
926msgid "Fold"
927msgstr "Replier"
928
929#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
930msgid "Edited: "
931msgstr "Modifié : "
932
933#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:180
934msgid "permalink"
935msgstr "permalien"
936
937#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
938msgid "Add tag"
939msgstr "Ajouter un tag"
940
941#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
942#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:7
943msgid "Filters"
944msgstr "Filtres"
945
946#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
947#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:12
948msgid "Filter private links"
949msgstr "Filtrer par liens privés"
950
951#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
952#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18
953msgid "Filter untagged links"
954msgstr "Filtrer par liens privés"
955
956#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
957#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
958#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:22
959#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:74
960#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
961#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
962msgid "Fold all"
963msgstr "Replier tout"
964
965#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
966#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:67
967msgid "Links per page"
968msgstr "Liens par page"
969
970#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
971msgid ""
972"You have been banned after too many failed login attempts. Try again later."
973msgstr ""
974"Vous avez été banni après trop d'échec d'authentification. Merci de "
975"réessayer plus tard."
976
977#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
978#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
979#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
980#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:95
981#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:71
982#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:95
983msgid "Login"
984msgstr "Connexion"
985
986#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
987#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151
988#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:151
989msgid "Remember me"
990msgstr "Rester connecté"
991
992#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
993#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
994#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14
995#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
996msgid "by the Shaarli community"
997msgstr "par la communauté Shaarli"
998
999#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
1000#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
1001msgid "Documentation"
1002msgstr "Documentation"
1003
1004#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
1005#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44
1006msgid "Expand"
1007msgstr "Déplier"
1008
1009#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45
1010#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45
1011msgid "Expand all"
1012msgstr "Déplier tout"
1013
1014#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
1015#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46
1016msgid "Are you sure you want to delete this link?"
1017msgstr "Êtes-vous sûr de vouloir supprimer ce lien ?"
1018
1019#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
1020#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31
1021msgid "Tools"
1022msgstr "Outils"
1023
1024#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
1025#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36
1026#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1027msgid "Tag cloud"
1028msgstr "Nuage de tags"
1029
1030#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
1031#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:39
1032msgid "Picture wall"
1033msgstr "Mur d'images"
1034
1035#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
1036#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:42
1037msgid "Daily"
1038msgstr "Quotidien"
1039
1040#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
1041#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
1042#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:61
1043#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:86
1044msgid "RSS Feed"
1045msgstr "Flux RSS"
1046
1047#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66
1048#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
1049#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:66
1050#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:102
1051msgid "Logout"
1052msgstr "Déconnexion"
1053
1054#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
1055#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169
1056msgid "is available"
1057msgstr "est disponible"
1058
1059#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
1060#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:176
1061msgid "Error"
1062msgstr "Erreur"
1063
1064#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1065msgid "Picture Wall"
1066msgstr "Mur d'images"
1067
1068#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1069msgid "pics"
1070msgstr "images"
1071
1072#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
1073msgid "You need to enable Javascript to change plugin loading order."
1074msgstr ""
1075"Vous devez activer Javascript pour pouvoir modifier l'ordre des extensions."
1076
1077#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
1078#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
1079msgid "Plugin administration"
1080msgstr "Administration des extensions"
1081
1082#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
1083msgid "Enabled Plugins"
1084msgstr "Extensions activées"
1085
1086#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
1087#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
1088msgid "No plugin enabled."
1089msgstr "Aucune extension activée."
1090
1091#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
1092#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73
1093msgid "Disable"
1094msgstr "Désactiver"
1095
1096#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1097#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
1098#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:98
1099#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
1100msgid "Name"
1101msgstr "Nom"
1102
1103#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
1104#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
1105msgid "Order"
1106msgstr "Ordre"
1107
1108#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
1109msgid "Disabled Plugins"
1110msgstr "Extensions désactivées"
1111
1112#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91
1113msgid "No plugin disabled."
1114msgstr "Aucune extension désactivée."
1115
1116#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97
1117#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
1118msgid "Enable"
1119msgstr "Activer"
1120
1121#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
1122msgid "More plugins available"
1123msgstr "Plus d'extensions disponibles"
1124
1125#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136
1126msgid "in the documentation"
1127msgstr "dans la documentation"
1128
1129#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
1130msgid "Plugin configuration"
1131msgstr "Configuration des extensions"
1132
1133#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1134#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1135msgid "tags"
1136msgstr "tags"
1137
1138#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
1139#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
1140msgid "List all links with those tags"
1141msgstr "Lister tous les liens avec ces tags"
1142
1143#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
1144msgid "Tag list"
1145msgstr "List des tags"
1146
1147#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3
1148#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3
1149msgid "Sort by:"
1150msgstr "Trier par :"
1151
1152#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5
1153#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5
1154msgid "Cloud"
1155msgstr "Nuage"
1156
1157#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:6
1158#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6
1159msgid "Most used"
1160msgstr "Plus utilisés"
1161
1162#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
1163#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7
1164msgid "Alphabetical"
1165msgstr "Alphabétique"
1166
1167#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
1168msgid "Settings"
1169msgstr "Paramètres"
1170
1171#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
1172msgid "Change Shaarli settings: title, timezone, etc."
1173msgstr "Changer les paramètres de Shaarli : titre, fuseau horaire, etc."
1174
1175#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
1176msgid "Configure your Shaarli"
1177msgstr "Conguration de Shaarli"
1178
1179#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
1180msgid "Enable, disable and configure plugins"
1181msgstr "Activer, désactiver et configurer les extensions"
1182
1183#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
1184msgid "Change your password"
1185msgstr "Modification du mot de passe"
1186
1187#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
1188msgid "Rename or delete a tag in all links"
1189msgstr "Rename or delete a tag in all links"
1190
1191#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
1192msgid ""
1193"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
1194"delicious...)"
1195msgstr ""
1196"Importer des marques pages au format Netscape HTML (comme exportés depuis "
1197"Firefox, Chrome, Opera, delicious...)"
1198
1199#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
1200msgid "Import links"
1201msgstr "Importer des liens"
1202
1203#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
1204msgid ""
1205"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
1206"Opera, delicious...)"
1207msgstr ""
1208"Exporter les marques pages au format Netscape HTML (comme exportés depuis "
1209"Firefox, Chrome, Opera, delicious...)"
1210
1211#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
1212msgid "Export database"
1213msgstr "Exporter les données"
1214
1215#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
1216msgid ""
1217"Drag one of these button to your bookmarks toolbar or right-click it and "
1218"\"Bookmark This Link\""
1219msgstr ""
1220"Glisser un de ces bouttons dans votre barre de favoris ou cliquer droit "
1221"dessus et « Ajouter aux favoris »"
1222
1223#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
1224msgid "then click on the bookmarklet in any page you want to share."
1225msgstr ""
1226"puis cliquer sur le marque page depuis un site que vous souhaitez partager."
1227
1228#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
1229#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:100
1230msgid ""
1231"Drag this link to your bookmarks toolbar or right-click it and Bookmark This "
1232"Link"
1233msgstr ""
1234"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
1235"Ajouter aux favoris »"
1236
1237#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
1238msgid "then click ✚Shaare link button in any page you want to share"
1239msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager"
1240
1241#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
1242#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108
1243msgid "The selected text is too long, it will be truncated."
1244msgstr "Le texte sélectionné est trop long, il sera tronqué."
1245
1246#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
1247msgid "Shaare link"
1248msgstr "Shaare"
1249
1250#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
1251msgid ""
1252"Then click ✚Add Note button anytime to start composing a private Note (text "
1253"post) to your Shaarli"
1254msgstr ""
1255"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli"
1256
1257#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
1258msgid "Add Note"
1259msgstr "Ajouter une Note"
1260
1261#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129
1262msgid ""
1263"You need to browse your Shaarli over <strong>HTTPS</strong> to use this "
1264"functionality."
1265msgstr ""
1266"Vous devez utiliser Shaarli en <strong>HTTPS</strong> pour utiliser cette "
1267"fonctionalité."
1268
1269#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
1270msgid "Add to"
1271msgstr "Ajouter à"
1272
1273#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145
1274msgid "3rd party"
1275msgstr "Applications tierces"
1276
1277#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
1278#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153
1279msgid "Plugin"
1280msgstr "Extension"
1281
1282#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148
1283#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154
1284msgid "plugin"
1285msgstr "extension"
1286
1287#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
1288msgid ""
1289"Drag this link to your bookmarks toolbar, or right-click it and choose "
1290"Bookmark This Link"
1291msgstr ""
1292"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
1293"Ajouter aux favoris »"
1294
1295#~ msgid ""
1296#~ "An error occurred while parsing JSON configuration file (%s): error code #"
1297#~ "%d"
1298#~ msgstr ""
1299#~ "Une erreur s'est produite lors de la lecture du fichier de configuration "
1300#~ "JSON (%s) : code d'erreur #%d"
1301
1302#~ msgid ""
1303#~ "Please check your JSON syntax (without PHP comment tags) using a JSON "
1304#~ "lint tool such as "
1305#~ msgstr ""
1306#~ "Merci de vérifier la syntaxe JSON (sans les balises de commentaires PHP) "
1307#~ "en utilisant un validateur de JSON tel que "
1308
1309#~ msgid ""
1310#~ "Error: missing Composer dependencies\n"
1311#~ "\n"
1312#~ "If you installed Shaarli through Git or using the development branch,\n"
1313#~ "please refer to the installation documentation to install PHP "
1314#~ "dependencies using Composer:\n"
1315#~ msgstr ""
1316#~ "Erreur : les dépendances Composer sont manquantes\n"
1317#~ "\n"
1318#~ "Si vous avez installé Shaarli avec Git ou depuis la branche de "
1319#~ "développement\n"
1320#~ "merci de consulter la documentation d'installation pour installer les "
1321#~ "dépendances Composer :\n"
1322#~ "\n"
1323
1324#~ msgid "Sessions do not seem to work correctly on your server."
1325#~ msgstr "Les sessions ne semblent "
1326
1327#~ msgid "Tag was renamed in "
1328#~ msgstr "Le tag a été renommé dans "
1329
1330#, fuzzy
1331#~| msgid "My links"
1332#~ msgid " links"
1333#~ msgstr "Mes liens"
1334
1335#, fuzzy
1336#~| msgid ""
1337#~| "Error: missing Composer configuration\n"
1338#~| "\n"
1339#~ msgid "Error: missing Composer configuration"
1340#~ msgstr ""
1341#~ "Erreur : la configuration Composer est manquante\n"
1342#~ "\n"
1343
1344#, fuzzy
1345#~| msgid ""
1346#~| "Shaarli could not create the config file. Please make sure Shaarli has "
1347#~| "the right to write in the folder is it installed in."
1348#~ msgid ""
1349#~ "Shaarli could not create the config file. \n"
1350#~ " Please make sure Shaarli has the right to write in the "
1351#~ "folder is it installed in."
1352#~ msgstr ""
1353#~ "Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier "
1354#~ "que Shaarli a les droits d'écriture dans le dossier dans lequel il est "
1355#~ "installé."
1356
1357#, fuzzy
1358#~| msgid "Plugin"
1359#~ msgid "Plugin \""
1360#~ msgstr "Extension"
1361
1362#~ msgid "Your PHP version is obsolete!"
1363#~ msgstr "Votre version de PHP est obsolète !"
1364
1365#~ msgid " Shaarli requires at least PHP "
1366#~ msgstr "Shaarli nécessite au moins PHP"
diff --git a/index.php b/index.php
index 4068a828..1dc81843 100644
--- a/index.php
+++ b/index.php
@@ -64,7 +64,6 @@ require_once 'application/FeedBuilder.php';
64require_once 'application/FileUtils.php'; 64require_once 'application/FileUtils.php';
65require_once 'application/History.php'; 65require_once 'application/History.php';
66require_once 'application/HttpUtils.php'; 66require_once 'application/HttpUtils.php';
67require_once 'application/Languages.php';
68require_once 'application/LinkDB.php'; 67require_once 'application/LinkDB.php';
69require_once 'application/LinkFilter.php'; 68require_once 'application/LinkFilter.php';
70require_once 'application/LinkUtils.php'; 69require_once 'application/LinkUtils.php';
@@ -76,6 +75,7 @@ require_once 'application/Utils.php';
76require_once 'application/PluginManager.php'; 75require_once 'application/PluginManager.php';
77require_once 'application/Router.php'; 76require_once 'application/Router.php';
78require_once 'application/Updater.php'; 77require_once 'application/Updater.php';
78use \Shaarli\Languages;
79use \Shaarli\ThemeUtils; 79use \Shaarli\ThemeUtils;
80use \Shaarli\Config\ConfigManager; 80use \Shaarli\Config\ConfigManager;
81 81
@@ -121,8 +121,16 @@ if (isset($_COOKIE['shaarli']) && !is_session_id_valid($_COOKIE['shaarli'])) {
121} 121}
122 122
123$conf = new ConfigManager(); 123$conf = new ConfigManager();
124
125// Sniff browser language and set date format accordingly.
126if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
127 autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
128}
129
130new Languages(setlocale(LC_MESSAGES, 0), $conf);
131
124$conf->setEmpty('general.timezone', date_default_timezone_get()); 132$conf->setEmpty('general.timezone', date_default_timezone_get());
125$conf->setEmpty('general.title', 'Shared links on '. escape(index_url($_SERVER))); 133$conf->setEmpty('general.title', t('Shared links on '). escape(index_url($_SERVER)));
126RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory 134RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
127RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory 135RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
128 136
@@ -144,7 +152,7 @@ if (! is_file($conf->getConfigFileExt())) {
144 $errors = ApplicationUtils::checkResourcePermissions($conf); 152 $errors = ApplicationUtils::checkResourcePermissions($conf);
145 153
146 if ($errors != array()) { 154 if ($errors != array()) {
147 $message = '<p>Insufficient permissions:</p><ul>'; 155 $message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
148 156
149 foreach ($errors as $error) { 157 foreach ($errors as $error) {
150 $message .= '<li>'.$error.'</li>'; 158 $message .= '<li>'.$error.'</li>';
@@ -163,11 +171,6 @@ if (! is_file($conf->getConfigFileExt())) {
163// a token depending of deployment salt, user password, and the current ip 171// a token depending of deployment salt, user password, and the current ip
164define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt'))); 172define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt')));
165 173
166// Sniff browser language and set date format accordingly.
167if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
168 autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
169}
170
171/** 174/**
172 * Checking session state (i.e. is the user still logged in) 175 * Checking session state (i.e. is the user still logged in)
173 * 176 *
@@ -376,7 +379,7 @@ function ban_canLogin($conf)
376// Process login form: Check if login/password is correct. 379// Process login form: Check if login/password is correct.
377if (isset($_POST['login'])) 380if (isset($_POST['login']))
378{ 381{
379 if (!ban_canLogin($conf)) die('I said: NO. You are banned for the moment. Go away.'); 382 if (!ban_canLogin($conf)) die(t('I said: NO. You are banned for the moment. Go away.'));
380 if (isset($_POST['password']) 383 if (isset($_POST['password'])
381 && tokenOk($_POST['token']) 384 && tokenOk($_POST['token'])
382 && (check_auth($_POST['login'], $_POST['password'], $conf)) 385 && (check_auth($_POST['login'], $_POST['password'], $conf))
@@ -440,7 +443,8 @@ if (isset($_POST['login']))
440 } 443 }
441 } 444 }
442 } 445 }
443 echo '<script>alert("Wrong login/password.");document.location=\'?do=login'.$redir.'\';</script>'; // Redirect to login screen. 446 // Redirect to login screen.
447 echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'?do=login'.$redir.'\';</script>';
444 exit; 448 exit;
445 } 449 }
446} 450}
@@ -1100,16 +1104,19 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1100 if ($targetPage == Router::$PAGE_CHANGEPASSWORD) 1104 if ($targetPage == Router::$PAGE_CHANGEPASSWORD)
1101 { 1105 {
1102 if ($conf->get('security.open_shaarli')) { 1106 if ($conf->get('security.open_shaarli')) {
1103 die('You are not supposed to change a password on an Open Shaarli.'); 1107 die(t('You are not supposed to change a password on an Open Shaarli.'));
1104 } 1108 }
1105 1109
1106 if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) 1110 if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword']))
1107 { 1111 {
1108 if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away! 1112 if (!tokenOk($_POST['token'])) die(t('Wrong token.')); // Go away!
1109 1113
1110 // Make sure old password is correct. 1114 // Make sure old password is correct.
1111 $oldhash = sha1($_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt')); 1115 $oldhash = sha1($_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt'));
1112 if ($oldhash!= $conf->get('credentials.hash')) { echo '<script>alert("The old password is not correct.");document.location=\'?do=changepasswd\';</script>'; exit; } 1116 if ($oldhash!= $conf->get('credentials.hash')) {
1117 echo '<script>alert("'. t('The old password is not correct.') .'");document.location=\'?do=changepasswd\';</script>';
1118 exit;
1119 }
1113 // Save new password 1120 // Save new password
1114 // Salt renders rainbow-tables attacks useless. 1121 // Salt renders rainbow-tables attacks useless.
1115 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); 1122 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
@@ -1127,7 +1134,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1127 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>'; 1134 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>';
1128 exit; 1135 exit;
1129 } 1136 }
1130 echo '<script>alert("Your password has been changed.");document.location=\'?do=tools\';</script>'; 1137 echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'?do=tools\';</script>';
1131 exit; 1138 exit;
1132 } 1139 }
1133 else // show the change password form. 1140 else // show the change password form.
@@ -1143,7 +1150,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1143 if (!empty($_POST['title']) ) 1150 if (!empty($_POST['title']) )
1144 { 1151 {
1145 if (!tokenOk($_POST['token'])) { 1152 if (!tokenOk($_POST['token'])) {
1146 die('Wrong token.'); // Go away! 1153 die(t('Wrong token.')); // Go away!
1147 } 1154 }
1148 $tz = 'UTC'; 1155 $tz = 'UTC';
1149 if (!empty($_POST['continent']) && !empty($_POST['city']) 1156 if (!empty($_POST['continent']) && !empty($_POST['city'])
@@ -1163,6 +1170,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1163 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks'])); 1170 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
1164 $conf->set('api.enabled', !empty($_POST['enableApi'])); 1171 $conf->set('api.enabled', !empty($_POST['enableApi']));
1165 $conf->set('api.secret', escape($_POST['apiSecret'])); 1172 $conf->set('api.secret', escape($_POST['apiSecret']));
1173 $conf->set('translation.language', escape($_POST['language']));
1174
1166 try { 1175 try {
1167 $conf->write(isLoggedIn()); 1176 $conf->write(isLoggedIn());
1168 $history->updateSettings(); 1177 $history->updateSettings();
@@ -1178,7 +1187,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1178 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>'; 1187 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>';
1179 exit; 1188 exit;
1180 } 1189 }
1181 echo '<script>alert("Configuration was saved.");document.location=\'?do=configure\';</script>'; 1190 echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'?do=configure\';</script>';
1182 exit; 1191 exit;
1183 } 1192 }
1184 else // Show the configuration form. 1193 else // Show the configuration form.
@@ -1200,6 +1209,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1200 $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false)); 1209 $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
1201 $PAGE->assign('api_enabled', $conf->get('api.enabled', true)); 1210 $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
1202 $PAGE->assign('api_secret', $conf->get('api.secret')); 1211 $PAGE->assign('api_secret', $conf->get('api.secret'));
1212 $PAGE->assign('languages', Languages::getAvailableLanguages());
1213 $PAGE->assign('language', $conf->get('translation.language'));
1203 $PAGE->renderPage('configure'); 1214 $PAGE->renderPage('configure');
1204 exit; 1215 exit;
1205 } 1216 }
@@ -1215,7 +1226,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1215 } 1226 }
1216 1227
1217 if (!tokenOk($_POST['token'])) { 1228 if (!tokenOk($_POST['token'])) {
1218 die('Wrong token.'); 1229 die(t('Wrong token.'));
1219 } 1230 }
1220 1231
1221 $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), escape($_POST['totag'])); 1232 $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), escape($_POST['totag']));
@@ -1225,9 +1236,10 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1225 } 1236 }
1226 $delete = empty($_POST['totag']); 1237 $delete = empty($_POST['totag']);
1227 $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag'])); 1238 $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
1239 $count = count($alteredLinks);
1228 $alert = $delete 1240 $alert = $delete
1229 ? sprintf(t('The tag was removed from %d links.'), count($alteredLinks)) 1241 ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d links.', $count), $count)
1230 : sprintf(t('The tag was renamed in %d links.'), count($alteredLinks)); 1242 : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d links.', $count), $count);
1231 echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>'; 1243 echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
1232 exit; 1244 exit;
1233 } 1245 }
@@ -1244,7 +1256,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1244 { 1256 {
1245 // Go away! 1257 // Go away!
1246 if (! tokenOk($_POST['token'])) { 1258 if (! tokenOk($_POST['token'])) {
1247 die('Wrong token.'); 1259 die(t('Wrong token.'));
1248 } 1260 }
1249 1261
1250 // lf_id should only be present if the link exists. 1262 // lf_id should only be present if the link exists.
@@ -1344,7 +1356,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1344 if ($targetPage == Router::$PAGE_DELETELINK) 1356 if ($targetPage == Router::$PAGE_DELETELINK)
1345 { 1357 {
1346 if (! tokenOk($_GET['token'])) { 1358 if (! tokenOk($_GET['token'])) {
1347 die('Wrong token.'); 1359 die(t('Wrong token.'));
1348 } 1360 }
1349 1361
1350 $ids = trim($_GET['lf_linkdate']); 1362 $ids = trim($_GET['lf_linkdate']);
@@ -1443,7 +1455,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1443 1455
1444 if ($url == '') { 1456 if ($url == '') {
1445 $url = '?' . smallHash($linkdate . $LINKSDB->getNextId()); 1457 $url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
1446 $title = $conf->get('general.default_note_title', 'Note: '); 1458 $title = $conf->get('general.default_note_title', t('Note: '));
1447 } 1459 }
1448 $url = escape($url); 1460 $url = escape($url);
1449 $title = escape($title); 1461 $title = escape($title);
@@ -1550,11 +1562,14 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1550 // Import bookmarks from an uploaded file 1562 // Import bookmarks from an uploaded file
1551 if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) { 1563 if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
1552 // The file is too big or some form field may be missing. 1564 // The file is too big or some form field may be missing.
1553 echo '<script>alert("The file you are trying to upload is probably' 1565 $msg = sprintf(
1554 .' bigger than what this webserver can accept (' 1566 t(
1555 .get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')).').' 1567 'The file you are trying to upload is probably bigger than what this webserver can accept'
1556 .' Please upload in smaller chunks.");document.location=\'?do=' 1568 .' (%s). Please upload in smaller chunks.'
1557 .Router::$PAGE_IMPORT .'\';</script>'; 1569 ),
1570 get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
1571 );
1572 echo '<script>alert("'. $msg .'");document.location=\'?do='.Router::$PAGE_IMPORT .'\';</script>';
1558 exit; 1573 exit;
1559 } 1574 }
1560 if (! tokenOk($_POST['token'])) { 1575 if (! tokenOk($_POST['token'])) {
@@ -1962,12 +1977,20 @@ function install($conf)
1962 // (Because on some hosts, session.save_path may not be set correctly, 1977 // (Because on some hosts, session.save_path may not be set correctly,
1963 // or we may not have write access to it.) 1978 // or we may not have write access to it.)
1964 if (isset($_GET['test_session']) && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) 1979 if (isset($_GET['test_session']) && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working'))
1965 { // Step 2: Check if data in session is correct. 1980 {
1966 echo '<pre>Sessions do not seem to work correctly on your server.<br>'; 1981 // Step 2: Check if data in session is correct.
1967 echo 'Make sure the variable session.save_path is set correctly in your php config, and that you have write access to it.<br>'; 1982 $msg = t(
1968 echo 'It currently points to '.session_save_path().'<br>'; 1983 '<pre>Sessions do not seem to work correctly on your server.<br>'.
1969 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.<br>'; 1984 'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
1970 echo '<br><a href="?">Click to try again.</a></pre>'; 1985 'and that you have write access to it.<br>'.
1986 'It currently points to %s.<br>'.
1987 'On some browsers, accessing your server via a hostname like \'localhost\' '.
1988 'or any custom hostname without a dot causes cookie storage to fail. '.
1989 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
1990 );
1991 $msg = sprintf($msg, session_save_path());
1992 echo $msg;
1993 echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
1971 die; 1994 die;
1972 } 1995 }
1973 if (!isset($_SESSION['session_tested'])) 1996 if (!isset($_SESSION['session_tested']))
@@ -2000,6 +2023,7 @@ function install($conf)
2000 } else { 2023 } else {
2001 $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER))); 2024 $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER)));
2002 } 2025 }
2026 $conf->set('translation.language', escape($_POST['language']));
2003 $conf->set('updates.check_updates', !empty($_POST['updateCheck'])); 2027 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
2004 $conf->set('api.enabled', !empty($_POST['enableApi'])); 2028 $conf->set('api.enabled', !empty($_POST['enableApi']));
2005 $conf->set( 2029 $conf->set(
@@ -2031,6 +2055,7 @@ function install($conf)
2031 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get()); 2055 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
2032 $PAGE->assign('continents', $continents); 2056 $PAGE->assign('continents', $continents);
2033 $PAGE->assign('cities', $cities); 2057 $PAGE->assign('cities', $cities);
2058 $PAGE->assign('languages', Languages::getAvailableLanguages());
2034 $PAGE->renderPage('install'); 2059 $PAGE->renderPage('install');
2035 exit; 2060 exit;
2036} 2061}
diff --git a/mkdocs.yml b/mkdocs.yml
index 03a7a34e..8617ea45 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -43,6 +43,7 @@ pages:
43 - Versioning and Branches: Versioning-and-Branches.md 43 - Versioning and Branches: Versioning-and-Branches.md
44 - Security: Security.md 44 - Security: Security.md
45 - Static analysis: Static-analysis.md 45 - Static analysis: Static-analysis.md
46 - Translations: Translations.md
46 - Theming: Theming.md 47 - Theming: Theming.md
47 - Unit tests: Unit-tests.md 48 - Unit tests: Unit-tests.md
48 - Unit tests inside Docker: Unit-tests-Docker.md 49 - Unit tests inside Docker: Unit-tests-Docker.md
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 @@
1https://github.com/shaarli/Shaarli/issues/181 - Add Disqus or Isso comments box on a permalink page
2
3 * http://posativ.org/isso/
4 * install debian package https://packages.debian.org/sid/isso
5 * configure server http://posativ.org/isso/docs/configuration/server/
6 * configure client http://posativ.org/isso/docs/configuration/client/
7 * http://posativ.org/isso/docs/quickstart/ and add `<script data-isso="//comments.example.tld/" src="//comments.example.tld/js/embed.min.js"></script>` to includes.html template; then add `<section id="isso-thread"></section>` in the linklist template where you want the comments (in the linklist_plugins loop for example)
8
9
10Problem: by default, Isso thread ID is guessed from the current url (only one thread per page).
11if we want multiple threads on a single page (shaarli linklist), we must use : the `data-isso-id` client config,
12with data-isso-id being the permalink of an item.
13
14`<section data-isso-id="aH7klxW" id="isso-thread"></section>`
15`data-isso-id: Set a custom thread id, defaults to current URI.`
16
17Problem: feature is currently broken https://github.com/posativ/isso/issues/27
18
19Another option, only display isso threads when current URL is a permalink (`\?(A-Z|a-z|0-9|-){7}`) (only show thread
20when displaying only this link), and just display a "comments" button on each linklist item. Optionally show the comment
21count on each item using the API (http://posativ.org/isso/docs/extras/api/#get-comment-count). API requests can be done
22by raintpl `{function` or client-side with js. The former should be faster if isso and shaarli are on ther same server.
23
24Showing all full isso threads in the linklist would destroy layout
25
26-----------------------------------------------------------
27
28http://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)
26 array( 26 array(
27 'type' => 'text', 27 'type' => 'text',
28 'name' => 'post', 28 'name' => 'post',
29 'placeholder' => 'URI', 29 'placeholder' => t('URI'),
30 ), 30 ),
31 array( 31 array(
32 'type' => 'submit', 32 'type' => 'submit',
33 'value' => 'Add link', 33 'value' => t('Add link'),
34 'class' => 'bigbutton', 34 'class' => 'bigbutton',
35 ), 35 ),
36 ), 36 ),
@@ -40,3 +40,12 @@ function hook_addlink_toolbar_render_header($data)
40 40
41 return $data; 41 return $data;
42} 42}
43
44/**
45 * This function is never called, but contains translation calls for GNU gettext extraction.
46 */
47function addlink_toolbar_dummy_translation()
48{
49 // meta
50 t('Adds the addlink input on the linklist page.');
51}
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 @@
1<span><a href="https://web.archive.org/web/%s"><img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="View on archive.org" alt="archive.org" /></a></span> 1<span>
2 <a href="https://web.archive.org/web/%s">
3 <img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
4 </a>
5</span>
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)
20 if($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) { 20 if($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) {
21 continue; 21 continue;
22 } 22 }
23 $archive = sprintf($archive_html, $value['url']); 23 $archive = sprintf($archive_html, $value['url'], t('View on archive.org'));
24 $value['link_plugin'][] = $archive; 24 $value['link_plugin'][] = $archive;
25 } 25 }
26 26
27 return $data; 27 return $data;
28} 28}
29
30/**
31 * This function is never called, but contains translation calls for GNU gettext extraction.
32 */
33function archiveorg_dummy_translation()
34{
35 // meta
36 t('For each link, add an Archive.org icon.');
37}
diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php
index 8fdbf663..b80a2b6d 100644
--- a/plugins/demo_plugin/demo_plugin.php
+++ b/plugins/demo_plugin/demo_plugin.php
@@ -14,6 +14,26 @@
14 * and check user status with _LOGGEDIN_. 14 * and check user status with _LOGGEDIN_.
15 */ 15 */
16 16
17use Shaarli\Config\ConfigManager;
18
19/**
20 * In the footer hook, there is a working example of a translation extension for Shaarli.
21 *
22 * The extension must be attached to a new translation domain (i.e. NOT 'shaarli').
23 * Use case: any custom theme or non official plugin can use the translation system.
24 *
25 * See the documentation for more information.
26 */
27const EXT_TRANSLATION_DOMAIN = 'demo';
28
29/*
30 * This is not necessary, but it's easier if you don't want Poedit to mix up your translations.
31 */
32function demo_plugin_t($text, $nText = '', $nb = 1)
33{
34 return t($text, $nText, $nb, EXT_TRANSLATION_DOMAIN);
35}
36
17/** 37/**
18 * Initialization function. 38 * Initialization function.
19 * It will be called when the plugin is loaded. 39 * It will be called when the plugin is loaded.
@@ -27,6 +47,12 @@ function demo_plugin_init($conf)
27{ 47{
28 $conf->get('toto', 'nope'); 48 $conf->get('toto', 'nope');
29 49
50 if (! $conf->exists('translation.extensions.demo')) {
51 // Custom translation with the domain 'demo'
52 $conf->set('translation.extensions.demo', 'plugins/demo_plugin/languages/');
53 $conf->write(true);
54 }
55
30 $errors[] = 'This a demo init error.'; 56 $errors[] = 'This a demo init error.';
31 return $errors; 57 return $errors;
32} 58}
@@ -160,7 +186,7 @@ function hook_demo_plugin_render_includes($data)
160function hook_demo_plugin_render_footer($data) 186function hook_demo_plugin_render_footer($data)
161{ 187{
162 // footer text 188 // footer text
163 $data['text'][] = 'Shaarli is now enhanced by the awesome demo_plugin.'; 189 $data['text'][] = '<br>'. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.');
164 190
165 // Free elements at the end of the page. 191 // Free elements at the end of the page.
166 $data['endofpage'][] = '<marquee id="demo_marquee">' . 192 $data['endofpage'][] = '<marquee id="demo_marquee">' .
@@ -433,3 +459,12 @@ function hook_demo_plugin_render_feed($data)
433 } 459 }
434 return $data; 460 return $data;
435} 461}
462
463/**
464 * This function is never called, but contains translation calls for GNU gettext extraction.
465 */
466function demo_dummy_translation()
467{
468 // meta
469 t('A demo plugin covering all use cases for template designers and plugin developers.');
470}
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 00000000..0f80f6ed
--- /dev/null
+++ b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mo
Binary files differ
diff --git a/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po
new file mode 100644
index 00000000..921379c0
--- /dev/null
+++ b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po
@@ -0,0 +1,21 @@
1msgid ""
2msgstr ""
3"Project-Id-Version: Demo plugin\n"
4"POT-Creation-Date: 2017-08-19 10:45+0200\n"
5"PO-Revision-Date: 2017-08-19 11:28+0200\n"
6"Last-Translator: \n"
7"Language-Team: demo\n"
8"Language: fr\n"
9"MIME-Version: 1.0\n"
10"Content-Type: text/plain; charset=UTF-8\n"
11"Content-Transfer-Encoding: 8bit\n"
12"X-Generator: Poedit 2.0.2\n"
13"X-Poedit-Basepath: ../../..\n"
14"Plural-Forms: nplurals=2; plural=(n > 1);\n"
15"X-Poedit-KeywordsList: ;demo_plugin_t:1,2;demo_plugin_t\n"
16"X-Poedit-SourceCharset: UTF-8\n"
17"X-Poedit-SearchPath-0: .\n"
18
19#: demo_plugin.php:173
20msgid "Shaarli is now enhanced by the awesome demo_plugin."
21msgstr "Shaarli est maintenant amélioré avec le fantastique demo_plugin."
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 @@
4 * Plugin Isso. 4 * Plugin Isso.
5 */ 5 */
6 6
7use Shaarli\Config\ConfigManager;
8
7/** 9/**
8 * Display an error everywhere if the plugin is enabled without configuration. 10 * Display an error everywhere if the plugin is enabled without configuration.
9 * 11 *
10 * @param $data array List of links
11 * @param $conf ConfigManager instance 12 * @param $conf ConfigManager instance
12 * 13 *
13 * @return mixed - linklist data with Isso plugin. 14 * @return mixed - linklist data with Isso plugin.
@@ -16,8 +17,8 @@ function isso_init($conf)
16{ 17{
17 $issoUrl = $conf->get('plugins.ISSO_SERVER'); 18 $issoUrl = $conf->get('plugins.ISSO_SERVER');
18 if (empty($issoUrl)) { 19 if (empty($issoUrl)) {
19 $error = 'Isso plugin error: '. 20 $error = t('Isso plugin error: '.
20 'Please define the "ISSO_SERVER" setting in the plugin administration page.'; 21 'Please define the "ISSO_SERVER" setting in the plugin administration page.');
21 return array($error); 22 return array($error);
22 } 23 }
23} 24}
@@ -52,3 +53,13 @@ function hook_isso_render_linklist($data, $conf)
52 53
53 return $data; 54 return $data;
54} 55}
56
57/**
58 * This function is never called, but contains translation calls for GNU gettext extraction.
59 */
60function isso_dummy_translation()
61{
62 // meta
63 t('Let visitor comment your shaares on permalinks with Isso.');
64 t('Isso server URL (without \'http://\')');
65}
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 @@
1<div class="md_help"> 1<div class="md_help">
2 Description will be rendered with 2 %s
3 <a href="http://daringfireball.net/projects/markdown/syntax" title="Markdown syntax documentation"> 3 <a href="http://daringfireball.net/projects/markdown/syntax" title="%s">
4 Markdown syntax</a>. 4 %s</a>.
5</div> 5</div>
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)
154function hook_markdown_render_editlink($data) 154function hook_markdown_render_editlink($data)
155{ 155{
156 // Load help HTML into a string 156 // Load help HTML into a string
157 $data['edit_link_plugin'][] = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html'); 157 $txt = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
158 158 $translations = [
159 t('Description will be rendered with'),
160 t('Markdown syntax documentation'),
161 t('Markdown syntax'),
162 ];
163 $data['edit_link_plugin'][] = vsprintf($txt, $translations);
159 // Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion. 164 // Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion.
160 if (! in_array(NO_MD_TAG, $data['tags'])) { 165 if (! in_array(NO_MD_TAG, $data['tags'])) {
161 $data['tags'][NO_MD_TAG] = 0; 166 $data['tags'][NO_MD_TAG] = 0;
@@ -325,3 +330,15 @@ function process_markdown($description, $escape = true, $allowedProtocols = [])
325 330
326 return $processedDescription; 331 return $processedDescription;
327} 332}
333
334/**
335 * This function is never called, but contains translation calls for GNU gettext extraction.
336 */
337function markdown_dummy_translation()
338{
339 // meta
340 t('Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
341If your shaared descriptions contained HTML tags before enabling the markdown plugin,
342enabling it might break your page.
343See the <a href="https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering">README</a>.');
344}
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)
18 $piwikUrl = $conf->get('plugins.PIWIK_URL'); 18 $piwikUrl = $conf->get('plugins.PIWIK_URL');
19 $piwikSiteid = $conf->get('plugins.PIWIK_SITEID'); 19 $piwikSiteid = $conf->get('plugins.PIWIK_SITEID');
20 if (empty($piwikUrl) || empty($piwikSiteid)) { 20 if (empty($piwikUrl) || empty($piwikSiteid)) {
21 $error = 'Piwik plugin error: ' . 21 $error = t('Piwik plugin error: ' .
22 'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.'; 22 'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.');
23 return array($error); 23 return array($error);
24 } 24 }
25} 25}
@@ -60,3 +60,14 @@ function hook_piwik_render_footer($data, $conf)
60 60
61 return $data; 61 return $data;
62} 62}
63
64/**
65 * This function is never called, but contains translation calls for GNU gettext extraction.
66 */
67function piwik_dummy_translation()
68{
69 // meta
70 t('A plugin that adds Piwik tracking code to Shaarli pages.');
71 t('Piwik URL');
72 t('Piwik site ID');
73}
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)
19 $playvideo = array( 19 $playvideo = array(
20 'attr' => array( 20 'attr' => array(
21 'href' => '#', 21 'href' => '#',
22 'title' => 'Video player', 22 'title' => t('Video player'),
23 'id' => 'playvideos', 23 'id' => 'playvideos',
24 ), 24 ),
25 'html' => 'â–º Play Videos' 25 'html' => 'â–º '. t('Play Videos')
26 ); 26 );
27 $data['buttons_toolbar'][] = $playvideo; 27 $data['buttons_toolbar'][] = $playvideo;
28 } 28 }
@@ -46,3 +46,12 @@ function hook_playvideos_render_footer($data)
46 46
47 return $data; 47 return $data;
48} 48}
49
50/**
51 * This function is never called, but contains translation calls for GNU gettext extraction.
52 */
53function playvideos_dummy_translation()
54{
55 // meta
56 t('Add a button in the toolbar allowing to watch all videos.');
57}
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 @@
10 */ 10 */
11 11
12use pubsubhubbub\publisher\Publisher; 12use pubsubhubbub\publisher\Publisher;
13use Shaarli\Config\ConfigManager;
13 14
14/** 15/**
15 * Plugin init function - set the hub to the default appspot one. 16 * Plugin init function - set the hub to the default appspot one.
@@ -65,7 +66,7 @@ function hook_pubsubhubbub_save_link($data, $conf)
65 $p = new Publisher($conf->get('plugins.PUBSUBHUB_URL')); 66 $p = new Publisher($conf->get('plugins.PUBSUBHUB_URL'));
66 $p->publish_update($feeds, $httpPost); 67 $p->publish_update($feeds, $httpPost);
67 } catch (Exception $e) { 68 } catch (Exception $e) {
68 error_log('Could not publish to PubSubHubbub: ' . $e->getMessage()); 69 error_log(sprintf(t('Could not publish to PubSubHubbub: %s'), $e->getMessage()));
69 } 70 }
70 71
71 return $data; 72 return $data;
@@ -91,11 +92,20 @@ function nocurl_http_post($url, $postString) {
91 $context = stream_context_create($params); 92 $context = stream_context_create($params);
92 $fp = @fopen($url, 'rb', false, $context); 93 $fp = @fopen($url, 'rb', false, $context);
93 if (!$fp) { 94 if (!$fp) {
94 throw new Exception('Could not post to '. $url); 95 throw new Exception(sprintf(t('Could not post to %s'), $url));
95 } 96 }
96 $response = @stream_get_contents($fp); 97 $response = @stream_get_contents($fp);
97 if ($response === false) { 98 if ($response === false) {
98 throw new Exception('Bad response from the hub '. $url); 99 throw new Exception(sprintf(t('Bad response from the hub %s'), $url));
99 } 100 }
100 return $response; 101 return $response;
101} 102}
103
104/**
105 * This function is never called, but contains translation calls for GNU gettext extraction.
106 */
107function pubsubhubbub_dummy_translation()
108{
109 // meta
110 t('Enable PubSubHubbub feed publishing.');
111}
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)
59 59
60 return $data; 60 return $data;
61} 61}
62
63/**
64 * This function is never called, but contains translation calls for GNU gettext extraction.
65 */
66function qrcode_dummy_translation()
67{
68 // meta
69 t('For each link, add a QRCode icon.');
70}
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 @@
1<span><a href="%s%s" target="_blank"><img class="linklist-plugin-icon" src="%s/wallabag/wallabag.png" title="Save to wallabag" alt="wallabag" /></a></span> 1<span>
2 <a href="%s%s" target="_blank">
3 <img class="linklist-plugin-icon" src="%s/wallabag/wallabag.png" title="%s" alt="wallabag" />
4 </a>
5</span>
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 @@
5 */ 5 */
6 6
7require_once 'WallabagInstance.php'; 7require_once 'WallabagInstance.php';
8use Shaarli\Config\ConfigManager;
8 9
9/** 10/**
10 * Init function, return an error if the server is not set. 11 * Init function, return an error if the server is not set.
@@ -17,8 +18,8 @@ function wallabag_init($conf)
17{ 18{
18 $wallabagUrl = $conf->get('plugins.WALLABAG_URL'); 19 $wallabagUrl = $conf->get('plugins.WALLABAG_URL');
19 if (empty($wallabagUrl)) { 20 if (empty($wallabagUrl)) {
20 $error = 'Wallabag plugin error: '. 21 $error = t('Wallabag plugin error: '.
21 'Please define the "WALLABAG_URL" setting in the plugin administration page.'; 22 'Please define the "WALLABAG_URL" setting in the plugin administration page.');
22 return array($error); 23 return array($error);
23 } 24 }
24} 25}
@@ -43,12 +44,14 @@ function hook_wallabag_render_linklist($data, $conf)
43 44
44 $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html'); 45 $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html');
45 46
47 $linkTitle = t('Save to wallabag');
46 foreach ($data['links'] as &$value) { 48 foreach ($data['links'] as &$value) {
47 $wallabag = sprintf( 49 $wallabag = sprintf(
48 $wallabagHtml, 50 $wallabagHtml,
49 $wallabagInstance->getWallabagUrl(), 51 $wallabagInstance->getWallabagUrl(),
50 urlencode($value['url']), 52 urlencode($value['url']),
51 PluginManager::$PLUGINS_PATH 53 PluginManager::$PLUGINS_PATH,
54 $linkTitle
52 ); 55 );
53 $value['link_plugin'][] = $wallabag; 56 $value['link_plugin'][] = $wallabag;
54 } 57 }
@@ -56,3 +59,14 @@ function hook_wallabag_render_linklist($data, $conf)
56 return $data; 59 return $data;
57} 60}
58 61
62/**
63 * This function is never called, but contains translation calls for GNU gettext extraction.
64 */
65function wallabag_dummy_translation()
66{
67 // meta
68 t('For each link, add a QRCode icon.');
69 t('Wallabag API URL');
70 t('Wallabag API version (1 or 2)');
71}
72
diff --git a/tests/LanguagesTest.php b/tests/LanguagesTest.php
index 79c136c8..864ce630 100644
--- a/tests/LanguagesTest.php
+++ b/tests/LanguagesTest.php
@@ -1,41 +1,203 @@
1<?php 1<?php
2 2
3require_once 'application/Languages.php'; 3namespace Shaarli;
4
5use Shaarli\Config\ConfigManager;
4 6
5/** 7/**
6 * Class LanguagesTest. 8 * Class LanguagesTest.
7 */ 9 */
8class LanguagesTest extends PHPUnit_Framework_TestCase 10class LanguagesTest extends \PHPUnit_Framework_TestCase
9{ 11{
10 /** 12 /**
13 * @var string Config file path (without extension).
14 */
15 protected static $configFile = 'tests/utils/config/configJson';
16
17 /**
18 * @var ConfigManager
19 */
20 protected $conf;
21
22 /**
23 *
24 */
25 public function setUp()
26 {
27 $this->conf = new ConfigManager(self::$configFile);
28 }
29
30 /**
31 * Test t() with a simple non identified value.
32 */
33 public function testTranslateSingleNotIDGettext()
34 {
35 $this->conf->set('translation.mode', 'gettext');
36 new Languages('en', $this->conf);
37 $text = 'abcdé 564 fgK';
38 $this->assertEquals($text, t($text));
39 }
40
41 /**
42 * Test t() with a simple identified value in gettext mode.
43 */
44 public function testTranslateSingleIDGettext()
45 {
46 $this->conf->set('translation.mode', 'gettext');
47 new Languages('en', $this->conf);
48 $text = 'permalink';
49 $this->assertEquals($text, t($text));
50 }
51
52 /**
53 * Test t() with a non identified plural form in gettext mode.
54 */
55 public function testTranslatePluralNotIDGettext()
56 {
57 $this->conf->set('translation.mode', 'gettext');
58 new Languages('en', $this->conf);
59 $text = 'sandwich';
60 $nText = 'sandwiches';
61 $this->assertEquals('sandwiches', t($text, $nText, 0));
62 $this->assertEquals('sandwich', t($text, $nText, 1));
63 $this->assertEquals('sandwiches', t($text, $nText, 2));
64 }
65
66 /**
67 * Test t() with an identified plural form in gettext mode.
68 */
69 public function testTranslatePluralIDGettext()
70 {
71 $this->conf->set('translation.mode', 'gettext');
72 new Languages('en', $this->conf);
73 $text = 'shaare';
74 $nText = 'shaares';
75 // In english, zero is followed by plural form
76 $this->assertEquals('shaares', t($text, $nText, 0));
77 $this->assertEquals('shaare', t($text, $nText, 1));
78 $this->assertEquals('shaares', t($text, $nText, 2));
79 }
80
81 /**
11 * Test t() with a simple non identified value. 82 * Test t() with a simple non identified value.
12 */ 83 */
13 public function testTranslateSingleNotID() 84 public function testTranslateSingleNotIDPhp()
14 { 85 {
86 $this->conf->set('translation.mode', 'php');
87 new Languages('en', $this->conf);
15 $text = 'abcdé 564 fgK'; 88 $text = 'abcdé 564 fgK';
16 $this->assertEquals($text, t($text)); 89 $this->assertEquals($text, t($text));
17 } 90 }
18 91
19 /** 92 /**
20 * Test t() with a non identified plural form. 93 * Test t() with a simple identified value in PHP mode.
21 */ 94 */
22 public function testTranslatePluralNotID() 95 public function testTranslateSingleIDPhp()
23 { 96 {
24 $text = '%s sandwich'; 97 $this->conf->set('translation.mode', 'php');
25 $nText = '%s sandwiches'; 98 new Languages('en', $this->conf);
26 $this->assertEquals('0 sandwich', t($text, $nText)); 99 $text = 'permalink';
27 $this->assertEquals('1 sandwich', t($text, $nText, 1)); 100 $this->assertEquals($text, t($text));
28 $this->assertEquals('2 sandwiches', t($text, $nText, 2));
29 } 101 }
30 102
31 /** 103 /**
32 * Test t() with a non identified invalid plural form. 104 * Test t() with a non identified plural form in PHP mode.
33 */ 105 */
34 public function testTranslatePluralNotIDInvalid() 106 public function testTranslatePluralNotIDPhp()
35 { 107 {
108 $this->conf->set('translation.mode', 'php');
109 new Languages('en', $this->conf);
36 $text = 'sandwich'; 110 $text = 'sandwich';
37 $nText = 'sandwiches'; 111 $nText = 'sandwiches';
112 $this->assertEquals('sandwiches', t($text, $nText, 0));
38 $this->assertEquals('sandwich', t($text, $nText, 1)); 113 $this->assertEquals('sandwich', t($text, $nText, 1));
39 $this->assertEquals('sandwiches', t($text, $nText, 2)); 114 $this->assertEquals('sandwiches', t($text, $nText, 2));
40 } 115 }
116
117 /**
118 * Test t() with an identified plural form in PHP mode.
119 */
120 public function testTranslatePluralIDPhp()
121 {
122 $this->conf->set('translation.mode', 'php');
123 new Languages('en', $this->conf);
124 $text = 'shaare';
125 $nText = 'shaares';
126 // In english, zero is followed by plural form
127 $this->assertEquals('shaares', t($text, $nText, 0));
128 $this->assertEquals('shaare', t($text, $nText, 1));
129 $this->assertEquals('shaares', t($text, $nText, 2));
130 }
131
132 /**
133 * Test t() with an invalid language set in the configuration in gettext mode.
134 */
135 public function testTranslateWithInvalidConfLanguageGettext()
136 {
137 $this->conf->set('translation.mode', 'gettext');
138 $this->conf->set('translation.language', 'nope');
139 new Languages('fr', $this->conf);
140 $text = 'grumble';
141 $this->assertEquals($text, t($text));
142 }
143
144 /**
145 * Test t() with an invalid language set in the configuration in PHP mode.
146 */
147 public function testTranslateWithInvalidConfLanguagePhp()
148 {
149 $this->conf->set('translation.mode', 'php');
150 $this->conf->set('translation.language', 'nope');
151 new Languages('fr', $this->conf);
152 $text = 'grumble';
153 $this->assertEquals($text, t($text));
154 }
155
156 /**
157 * Test t() with an invalid language set with auto language in gettext mode.
158 */
159 public function testTranslateWithInvalidAutoLanguageGettext()
160 {
161 $this->conf->set('translation.mode', 'gettext');
162 new Languages('nope', $this->conf);
163 $text = 'grumble';
164 $this->assertEquals($text, t($text));
165 }
166
167 /**
168 * Test t() with an invalid language set with auto language in PHP mode.
169 */
170 public function testTranslateWithInvalidAutoLanguagePhp()
171 {
172 $this->conf->set('translation.mode', 'php');
173 new Languages('nope', $this->conf);
174 $text = 'grumble';
175 $this->assertEquals($text, t($text));
176 }
177
178 /**
179 * Test t() with an extension language file in gettext mode
180 */
181 public function testTranslationExtensionGettext()
182 {
183 $this->conf->set('translation.mode', 'gettext');
184 $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
185 new Languages('en', $this->conf);
186 $txt = 'car'; // ignore me poedit
187 $this->assertEquals('car', t($txt, $txt, 1, 'test'));
188 $this->assertEquals('Search', t('Search', 'Search', 1, 'test'));
189 }
190
191 /**
192 * Test t() with an extension language file in PHP mode
193 */
194 public function testTranslationExtensionPhp()
195 {
196 $this->conf->set('translation.mode', 'php');
197 $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
198 new Languages('en', $this->conf);
199 $txt = 'car'; // ignore me poedit
200 $this->assertEquals('car', t($txt, $txt, 1, 'test'));
201 $this->assertEquals('Search', t('Search', 'Search', 1, 'test'));
202 }
41} 203}
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 @@ class UtilsTest extends PHPUnit_Framework_TestCase
384 */ 384 */
385 public function testHumanBytes() 385 public function testHumanBytes()
386 { 386 {
387 $this->assertEquals('2kiB', human_bytes(2 * 1024)); 387 $this->assertEquals('2'. t('kiB'), human_bytes(2 * 1024));
388 $this->assertEquals('2kiB', human_bytes(strval(2 * 1024))); 388 $this->assertEquals('2'. t('kiB'), human_bytes(strval(2 * 1024)));
389 $this->assertEquals('2MiB', human_bytes(2 * (pow(1024, 2)))); 389 $this->assertEquals('2'. t('MiB'), human_bytes(2 * (pow(1024, 2))));
390 $this->assertEquals('2MiB', human_bytes(strval(2 * (pow(1024, 2))))); 390 $this->assertEquals('2'. t('MiB'), human_bytes(strval(2 * (pow(1024, 2)))));
391 $this->assertEquals('2GiB', human_bytes(2 * (pow(1024, 3)))); 391 $this->assertEquals('2'. t('GiB'), human_bytes(2 * (pow(1024, 3))));
392 $this->assertEquals('2GiB', human_bytes(strval(2 * (pow(1024, 3))))); 392 $this->assertEquals('2'. t('GiB'), human_bytes(strval(2 * (pow(1024, 3)))));
393 $this->assertEquals('374B', human_bytes(374)); 393 $this->assertEquals('374'. t('B'), human_bytes(374));
394 $this->assertEquals('374B', human_bytes('374')); 394 $this->assertEquals('374'. t('B'), human_bytes('374'));
395 $this->assertEquals('232kiB', human_bytes(237481)); 395 $this->assertEquals('232'. t('kiB'), human_bytes(237481));
396 $this->assertEquals('Unlimited', human_bytes('0')); 396 $this->assertEquals(t('Unlimited'), human_bytes('0'));
397 $this->assertEquals('Unlimited', human_bytes(0)); 397 $this->assertEquals(t('Unlimited'), human_bytes(0));
398 $this->assertEquals('Setting not set', human_bytes('')); 398 $this->assertEquals(t('Setting not set'), human_bytes(''));
399 } 399 }
400 400
401 /** 401 /**
@@ -403,9 +403,9 @@ class UtilsTest extends PHPUnit_Framework_TestCase
403 */ 403 */
404 public function testGetMaxUploadSize() 404 public function testGetMaxUploadSize()
405 { 405 {
406 $this->assertEquals('1MiB', get_max_upload_size(2097152, '1024k')); 406 $this->assertEquals('1'. t('MiB'), get_max_upload_size(2097152, '1024k'));
407 $this->assertEquals('1MiB', get_max_upload_size('1m', '2m')); 407 $this->assertEquals('1'. t('MiB'), get_max_upload_size('1m', '2m'));
408 $this->assertEquals('100B', get_max_upload_size(100, 100)); 408 $this->assertEquals('100'. t('B'), get_max_upload_size(100, 100));
409 } 409 }
410 410
411 /** 411 /**
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 @@
1<?php
2
3require_once 'vendor/autoload.php';
4
5$conf = new \Shaarli\Config\ConfigManager('tests/utils/config/configJson');
6new \Shaarli\Languages('en', $conf);
diff --git a/tests/languages/bootstrap.php b/tests/languages/bootstrap.php
index 95609210..da6ac2e4 100644
--- a/tests/languages/bootstrap.php
+++ b/tests/languages/bootstrap.php
@@ -1,7 +1,6 @@
1<?php 1<?php
2if (! empty('UT_LOCALE')) { 2require_once 'tests/bootstrap.php';
3
4if (! empty(getenv('UT_LOCALE'))) {
3 setlocale(LC_ALL, getenv('UT_LOCALE')); 5 setlocale(LC_ALL, getenv('UT_LOCALE'));
4} 6}
5
6require_once 'vendor/autoload.php';
7
diff --git a/tests/languages/fr/LanguagesFrTest.php b/tests/languages/fr/LanguagesFrTest.php
new file mode 100644
index 00000000..79d05172
--- /dev/null
+++ b/tests/languages/fr/LanguagesFrTest.php
@@ -0,0 +1,175 @@
1<?php
2
3
4namespace Shaarli;
5
6
7use Shaarli\Config\ConfigManager;
8
9/**
10 * Class LanguagesFrTest
11 *
12 * Test the translation system in PHP and gettext mode with French language.
13 *
14 * @package Shaarli
15 */
16class LanguagesFrTest extends \PHPUnit_Framework_TestCase
17{
18 /**
19 * @var string Config file path (without extension).
20 */
21 protected static $configFile = 'tests/utils/config/configJson';
22
23 /**
24 * @var ConfigManager
25 */
26 protected $conf;
27
28 /**
29 * Init: force French
30 */
31 public function setUp()
32 {
33 $this->conf = new ConfigManager(self::$configFile);
34 $this->conf->set('translation.language', 'fr');
35 }
36
37 /**
38 * Reset the locale since gettext seems to mess with it, making it too long
39 */
40 public static function tearDownAfterClass()
41 {
42 if (! empty(getenv('UT_LOCALE'))) {
43 setlocale(LC_ALL, getenv('UT_LOCALE'));
44 }
45 }
46
47 /**
48 * Test t() with a simple non identified value.
49 */
50 public function testTranslateSingleNotIDGettext()
51 {
52 $this->conf->set('translation.mode', 'gettext');
53 new Languages('en', $this->conf);
54 $text = 'abcdé 564 fgK';
55 $this->assertEquals($text, t($text));
56 }
57
58 /**
59 * Test t() with a simple identified value in gettext mode.
60 */
61 public function testTranslateSingleIDGettext()
62 {
63 $this->conf->set('translation.mode', 'gettext');
64 new Languages('en', $this->conf);
65 $text = 'permalink';
66 $this->assertEquals('permalien', t($text));
67 }
68
69 /**
70 * Test t() with a non identified plural form in gettext mode.
71 */
72 public function testTranslatePluralNotIDGettext()
73 {
74 $this->conf->set('translation.mode', 'gettext');
75 new Languages('en', $this->conf);
76 $text = 'sandwich';
77 $nText = 'sandwiches';
78 // Not ID, so English fallback, and in english, plural 0
79 $this->assertEquals('sandwiches', t($text, $nText, 0));
80 $this->assertEquals('sandwich', t($text, $nText, 1));
81 $this->assertEquals('sandwiches', t($text, $nText, 2));
82 }
83
84 /**
85 * Test t() with an identified plural form in gettext mode.
86 */
87 public function testTranslatePluralIDGettext()
88 {
89 $this->conf->set('translation.mode', 'gettext');
90 new Languages('en', $this->conf);
91 $text = 'shaare';
92 $nText = 'shaares';
93 $this->assertEquals('shaare', t($text, $nText, 0));
94 $this->assertEquals('shaare', t($text, $nText, 1));
95 $this->assertEquals('shaares', t($text, $nText, 2));
96 }
97
98 /**
99 * Test t() with a simple non identified value.
100 */
101 public function testTranslateSingleNotIDPhp()
102 {
103 $this->conf->set('translation.mode', 'php');
104 new Languages('en', $this->conf);
105 $text = 'abcdé 564 fgK';
106 $this->assertEquals($text, t($text));
107 }
108
109 /**
110 * Test t() with a simple identified value in PHP mode.
111 */
112 public function testTranslateSingleIDPhp()
113 {
114 $this->conf->set('translation.mode', 'php');
115 new Languages('en', $this->conf);
116 $text = 'permalink';
117 $this->assertEquals('permalien', t($text));
118 }
119
120 /**
121 * Test t() with a non identified plural form in PHP mode.
122 */
123 public function testTranslatePluralNotIDPhp()
124 {
125 $this->conf->set('translation.mode', 'php');
126 new Languages('en', $this->conf);
127 $text = 'sandwich';
128 $nText = 'sandwiches';
129 // Not ID, so English fallback, and in english, plural 0
130 $this->assertEquals('sandwiches', t($text, $nText, 0));
131 $this->assertEquals('sandwich', t($text, $nText, 1));
132 $this->assertEquals('sandwiches', t($text, $nText, 2));
133 }
134
135 /**
136 * Test t() with an identified plural form in PHP mode.
137 */
138 public function testTranslatePluralIDPhp()
139 {
140 $this->conf->set('translation.mode', 'php');
141 new Languages('en', $this->conf);
142 $text = 'shaare';
143 $nText = 'shaares';
144 // In english, zero is followed by plural form
145 $this->assertEquals('shaare', t($text, $nText, 0));
146 $this->assertEquals('shaare', t($text, $nText, 1));
147 $this->assertEquals('shaares', t($text, $nText, 2));
148 }
149
150 /**
151 * Test t() with an extension language file in gettext mode
152 */
153 public function testTranslationExtensionGettext()
154 {
155 $this->conf->set('translation.mode', 'gettext');
156 $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
157 new Languages('en', $this->conf);
158 $txt = 'car'; // ignore me poedit
159 $this->assertEquals('voiture', t($txt, $txt, 1, 'test'));
160 $this->assertEquals('Fouille', t('Search', 'Search', 1, 'test'));
161 }
162
163 /**
164 * Test t() with an extension language file in PHP mode
165 */
166 public function testTranslationExtensionPhp()
167 {
168 $this->conf->set('translation.mode', 'php');
169 $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
170 new Languages('en', $this->conf);
171 $txt = 'car'; // ignore me poedit
172 $this->assertEquals('voiture', t($txt, $txt, 1, 'test'));
173 $this->assertEquals('Fouille', t('Search', 'Search', 1, 'test'));
174 }
175}
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 00000000..416c7831
--- /dev/null
+++ b/tests/utils/languages/fr/LC_MESSAGES/test.mo
Binary files differ
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 @@
1msgid ""
2msgstr ""
3"Project-Id-Version: Extension test\n"
4"POT-Creation-Date: 2017-05-20 13:54+0200\n"
5"PO-Revision-Date: 2017-05-20 14:16+0200\n"
6"Last-Translator: \n"
7"Language-Team: Shaarli\n"
8"Language: fr_FR\n"
9"MIME-Version: 1.0\n"
10"Content-Type: text/plain; charset=UTF-8\n"
11"Content-Transfer-Encoding: 8bit\n"
12"Plural-Forms: nplurals=2; plural=(n > 1);\n"
13"X-Generator: Poedit 2.0.1\n"
14
15msgid "car"
16msgstr "voiture"
17
18msgid "Search"
19msgstr "Fouille"
diff --git a/tpl/default/changetag.html b/tpl/default/changetag.html
index 49dd20d9..6606c4fa 100644
--- a/tpl/default/changetag.html
+++ b/tpl/default/changetag.html
@@ -32,7 +32,7 @@
32 </div> 32 </div>
33 </form> 33 </form>
34 34
35 <p>You can also edit tags in the <a href="?do=taglist&sort=usage">tag list</a>.</p> 35 <p>{'You can also edit tags in the'|t} <a href="?do=taglist&sort=usage">{'tag list'|t}</a>.</p>
36 </div> 36 </div>
37</div> 37</div>
38{include="page.footer"} 38{include="page.footer"}
diff --git a/tpl/default/configure.html b/tpl/default/configure.html
index 76a1b9fd..cc3b299b 100644
--- a/tpl/default/configure.html
+++ b/tpl/default/configure.html
@@ -70,6 +70,30 @@
70 </div> 70 </div>
71 </div> 71 </div>
72 <div class="pure-g"> 72 <div class="pure-g">
73 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
74 <div class="form-label">
75 <label for="language">
76 <span class="label-name">{'Language'|t}</span>
77 </label>
78 </div>
79 </div>
80 <div class="pure-u-lg-{$ratioInput} pure-u-1">
81 <div class="form-input">
82 <select name="language" id="language" class="align">
83 {loop="$languages"}
84 <option value="{$key}"
85 {if="$key===$language"}
86 selected="selected"
87 {/if}
88 >
89 {$value}
90 </option>
91 {/loop}
92 </select>
93 </div>
94 </div>
95 </div>
96 <div class="pure-g">
73 <div class="pure-u-lg-{$ratioLabel} pure-u-1 "> 97 <div class="pure-u-lg-{$ratioLabel} pure-u-1 ">
74 <div class="form-label"> 98 <div class="form-label">
75 <label> 99 <label>
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 @@
18 <div class="center" id="import-field"> 18 <div class="center" id="import-field">
19 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}"> 19 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
20 <input type="file" name="filetoupload"> 20 <input type="file" name="filetoupload">
21 <p><br>Maximum size allowed: <strong>{$maxfilesizeHuman}</strong></p> 21 <p><br>{'Maximum size allowed:'|t} <strong>{$maxfilesizeHuman}</strong></p>
22 </div> 22 </div>
23 23
24 <div class="pure-g"> 24 <div class="pure-g">
@@ -31,15 +31,15 @@
31 <div class="radio-buttons"> 31 <div class="radio-buttons">
32 <div> 32 <div>
33 <input type="radio" name="privacy" value="default" checked="checked"> 33 <input type="radio" name="privacy" value="default" checked="checked">
34 Use values from the imported file, default to public 34 {'Use values from the imported file, default to public'|t}
35 </div> 35 </div>
36 <div> 36 <div>
37 <input type="radio" name="privacy" value="private"> 37 <input type="radio" name="privacy" value="private">
38 Import all bookmarks as private 38 {'Import all bookmarks as private'|t}
39 </div> 39 </div>
40 <div> 40 <div>
41 <input type="radio" name="privacy" value="public"> 41 <input type="radio" name="privacy" value="public">
42 Import all bookmarks as public 42 {'Import all bookmarks as public'|t}
43 </div> 43 </div>
44 </div> 44 </div>
45 </div> 45 </div>
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
@@ -68,6 +68,27 @@
68 <div class="pure-g"> 68 <div class="pure-g">
69 <div class="pure-u-lg-{$ratioLabel} pure-u-1"> 69 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
70 <div class="form-label"> 70 <div class="form-label">
71 <label for="language">
72 <span class="label-name">{'Language'|t}</span>
73 </label>
74 </div>
75 </div>
76 <div class="pure-u-lg-{$ratioInput} pure-u-1">
77 <div class="form-input">
78 <select name="language" id="language" class="align">
79 {loop="$languages"}
80 <option value="{$key}">
81 {$value}
82 </option>
83 {/loop}
84 </select>
85 </div>
86 </div>
87 </div>
88
89 <div class="pure-g">
90 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
91 <div class="form-label">
71 <label> 92 <label>
72 <span class="label-name">{'Timezone'|t}</span><br> 93 <span class="label-name">{'Timezone'|t}</span><br>
73 <span class="label-desc">{'Continent'|t} &middot; {'City'|t}</span> 94 <span class="label-desc">{'Continent'|t} &middot; {'City'|t}</span>
diff --git a/tpl/default/js/shaarli.js b/tpl/default/js/shaarli.js
index 55656f80..09b07eed 100644
--- a/tpl/default/js/shaarli.js
+++ b/tpl/default/js/shaarli.js
@@ -138,6 +138,9 @@ window.onload = function () {
138 }); 138 });
139 foldAllButton.firstElementChild.classList.toggle('fa-chevron-down'); 139 foldAllButton.firstElementChild.classList.toggle('fa-chevron-down');
140 foldAllButton.firstElementChild.classList.toggle('fa-chevron-up'); 140 foldAllButton.firstElementChild.classList.toggle('fa-chevron-up');
141 foldAllButton.title = state === 'down'
142 ? document.getElementById('translation-fold-all').innerHTML
143 : document.getElementById('translation-expand-all').innerHTML
141 }); 144 });
142 }); 145 });
143 } 146 }
@@ -146,7 +149,7 @@ window.onload = function () {
146 { 149 {
147 // Switch fold/expand - up = fold 150 // Switch fold/expand - up = fold
148 if (button.classList.contains('fa-chevron-up')) { 151 if (button.classList.contains('fa-chevron-up')) {
149 button.title = 'Expand'; 152 button.title = document.getElementById('translation-expand').innerHTML;
150 if (description != null) { 153 if (description != null) {
151 description.style.display = 'none'; 154 description.style.display = 'none';
152 } 155 }
@@ -155,7 +158,7 @@ window.onload = function () {
155 } 158 }
156 } 159 }
157 else { 160 else {
158 button.title = 'Fold'; 161 button.title = document.getElementById('translation-fold').innerHTML;
159 if (description != null) { 162 if (description != null) {
160 description.style.display = 'block'; 163 description.style.display = 'block';
161 } 164 }
@@ -173,7 +176,7 @@ window.onload = function () {
173 var deleteLinks = document.querySelectorAll('.confirm-delete'); 176 var deleteLinks = document.querySelectorAll('.confirm-delete');
174 [].forEach.call(deleteLinks, function(deleteLink) { 177 [].forEach.call(deleteLinks, function(deleteLink) {
175 deleteLink.addEventListener('click', function(event) { 178 deleteLink.addEventListener('click', function(event) {
176 if(! confirm('Are you sure you want to delete this link ?')) { 179 if(! confirm(document.getElementById('translation-delete-link').innerHTML)) {
177 event.preventDefault(); 180 event.preventDefault();
178 } 181 }
179 }); 182 });
@@ -618,7 +621,7 @@ function activateFirefoxSocial(node) {
618 // Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable. 621 // Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable.
619 var data = { 622 var data = {
620 name: title, 623 name: title,
621 description: "The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community.", 624 description: document.getElementById('translation-delete-link').innerHTML,
622 author: "Shaarli", 625 author: "Shaarli",
623 version: "1.0.0", 626 version: "1.0.0",
624 627
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 @@
86 <div class="pure-g pure-alert pure-alert-success search-result"> 86 <div class="pure-g pure-alert pure-alert-success search-result">
87 <div class="pure-u-2-24"></div> 87 <div class="pure-u-2-24"></div>
88 <div class="pure-u-20-24"> 88 <div class="pure-u-20-24">
89 {function="t('%s result', '%s results', $result_count)"} 89 {function="sprintf(t('%s result', '%s results', $result_count), $result_count)"}
90 {if="!empty($search_term)"} 90 {if="!empty($search_term)"}
91 {'for'|t} <em><strong>{$search_term}</strong></em> 91 {'for'|t} <em><strong>{$search_term}</strong></em>
92 {/if} 92 {/if}
@@ -117,6 +117,16 @@
117 <div class="pure-g"> 117 <div class="pure-g">
118 <div class="pure-u-lg-2-24 pure-u-1-24"></div> 118 <div class="pure-u-lg-2-24 pure-u-1-24"></div>
119 <div class="pure-u-lg-20-24 pure-u-22-24"> 119 <div class="pure-u-lg-20-24 pure-u-22-24">
120 {ignore}Set translation here, for performances{/ignore}
121 {$strPrivate=t('Private')}
122 {$strEdit=t('Edit')}
123 {$strDelete=t('Delete')}
124 {$strFold=t('Fold')}
125 {$strEdited=t('Edited: ')}
126 {$strPermalink=t('Permalink')}
127 {$strPermalinkLc=t('permalink')}
128 {$strAddTag=t('Add tag')}
129 {ignore}End of translations{/ignore}
120 {loop="links"} 130 {loop="links"}
121 <div class="anchor" id="{$value.shorturl}"></div> 131 <div class="anchor" id="{$value.shorturl}"></div>
122 <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}"> 132 <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}">
@@ -125,12 +135,12 @@
125 {if="isLoggedIn()"} 135 {if="isLoggedIn()"}
126 <div class="linklist-item-editbuttons"> 136 <div class="linklist-item-editbuttons">
127 {if="$value.private"} 137 {if="$value.private"}
128 <span class="label label-private">{'Private'|t}</span> 138 <span class="label label-private">{$strPrivate}</span>
129 {/if} 139 {/if}
130 <input type="checkbox" class="delete-checkbox" value="{$value.id}"> 140 <input type="checkbox" class="delete-checkbox" value="{$value.id}">
131 <!-- FIXME! JS translation --> 141 <!-- FIXME! JS translation -->
132 <a href="?edit_link={$value.id}" title="{'Edit'|t}"><i class="fa fa-pencil-square-o edit-link"></i></a> 142 <a href="?edit_link={$value.id}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link"></i></a>
133 <a href="#" title="{'Fold'|t}" class="fold-button"><i class="fa fa-chevron-up"></i></a> 143 <a href="#" title="{$strFold}" class="fold-button"><i class="fa fa-chevron-up"></i></a>
134 </div> 144 </div>
135 {/if} 145 {/if}
136 146
@@ -164,7 +174,7 @@
164 <i class="fa fa-tags"></i> 174 <i class="fa fa-tags"></i>
165 {$tag_counter=count($value.taglist)} 175 {$tag_counter=count($value.taglist)}
166 {loop="value.taglist"} 176 {loop="value.taglist"}
167 <span class="label label-tag" title="Add tag"> 177 <span class="label label-tag" title="{$strAddTag}">
168 <a href="?addtag={$value|urlencode}">{$value}</a> 178 <a href="?addtag={$value|urlencode}">{$value}</a>
169 </span> 179 </span>
170 {if="$tag_counter - 1 != $counter"}&middot;{/if} 180 {if="$tag_counter - 1 != $counter"}&middot;{/if}
@@ -174,9 +184,9 @@
174 184
175 <div class="pure-g"> 185 <div class="pure-g">
176 <div class="linklist-item-infos-dateblock pure-u-lg-3-8 pure-u-1"> 186 <div class="linklist-item-infos-dateblock pure-u-lg-3-8 pure-u-1">
177 <a href="?{$value.shorturl}" title="{'Permalink'|t}"> 187 <a href="?{$value.shorturl}" title="{$strPermalink}">
178 {if="!$hide_timestamps || isLoggedIn()"} 188 {if="!$hide_timestamps || isLoggedIn()"}
179 {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'} 189 {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink}
180 <span class="linkdate" title="{$updated}"> 190 <span class="linkdate" title="{$updated}">
181 <i class="fa fa-clock-o"></i> 191 <i class="fa fa-clock-o"></i>
182 {$value.created|format_date} 192 {$value.created|format_date}
@@ -184,7 +194,7 @@
184 &middot; 194 &middot;
185 </span> 195 </span>
186 {/if} 196 {/if}
187 {'permalink'|t} 197 {$strPermalinkLc}
188 </a> 198 </a>
189 199
190 <div class="pure-u-0 pure-u-lg-visible"> 200 <div class="pure-u-0 pure-u-lg-visible">
@@ -205,7 +215,7 @@
205 </a> 215 </a>
206 {if="isLoggedIn()"} 216 {if="isLoggedIn()"}
207 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}" 217 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}"
208 title="{'Delete'|t}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete"> 218 title="{$strDelete}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete">
209 <i class="fa fa-trash"></i> 219 <i class="fa fa-trash"></i>
210 </a> 220 </a>
211 {/if} 221 {/if}
@@ -221,7 +231,7 @@
221 {if="isLoggedIn()"} 231 {if="isLoggedIn()"}
222 &middot; 232 &middot;
223 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}" 233 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}"
224 title="{'Delete'|t}" class="delete-link confirm-delete"> 234 title="{$strDelete}" class="delete-link confirm-delete">
225 <i class="fa fa-trash"></i> 235 <i class="fa fa-trash"></i>
226 </a> 236 </a>
227 {/if} 237 {/if}
diff --git a/tpl/default/linklist.paging.html b/tpl/default/linklist.paging.html
index 41e9fa34..347b3d13 100644
--- a/tpl/default/linklist.paging.html
+++ b/tpl/default/linklist.paging.html
@@ -13,7 +13,7 @@
13 <a href="?untaggedonly" title="{'Filter untagged links'|t}" 13 <a href="?untaggedonly" title="{'Filter untagged links'|t}"
14 class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if} 14 class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if}
15 ><i class="fa fa-tag"></i></a> 15 ><i class="fa fa-tag"></i></a>
16 <a href="#" class="filter-off fold-all pure-u-lg-0" title="Fold all"> 16 <a href="#" class="filter-off fold-all pure-u-lg-0" title="{'Fold all'|t}">
17 <i class="fa fa-chevron-up"></i> 17 <i class="fa fa-chevron-up"></i>
18 </a> 18 </a>
19 {loop="$action_plugin"} 19 {loop="$action_plugin"}
@@ -53,7 +53,7 @@
53 <form method="GET" class="pure-u-0 pure-u-lg-visible"> 53 <form method="GET" class="pure-u-0 pure-u-lg-visible">
54 <input type="text" name="linksperpage" placeholder="133"> 54 <input type="text" name="linksperpage" placeholder="133">
55 </form> 55 </form>
56 <a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" title="Fold all"> 56 <a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" title="{'Fold all'|t}">
57 <i class="fa fa-chevron-up"></i> 57 <i class="fa fa-chevron-up"></i>
58 </a> 58 </a>
59 </div> 59 </div>
diff --git a/tpl/default/page.footer.html b/tpl/default/page.footer.html
index 54b16e8a..659e8c7f 100644
--- a/tpl/default/page.footer.html
+++ b/tpl/default/page.footer.html
@@ -8,8 +8,8 @@
8 {$version} 8 {$version}
9 {/if} 9 {/if}
10 &middot; 10 &middot;
11 The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community &middot; 11 {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} &middot;
12 <a href="doc/html/index.html" rel="nofollow">Documentation</a> 12 <a href="doc/html/index.html" rel="nofollow">{'Documentation'|t}</a>
13 {loop="$plugins_footer.text"} 13 {loop="$plugins_footer.text"}
14 {$value} 14 {$value}
15 {/loop} 15 {/loop}
@@ -27,6 +27,17 @@
27 <script src="{$value}#"></script> 27 <script src="{$value}#"></script>
28{/loop} 28{/loop}
29 29
30<div id="js-translations" class="hidden">
31 <span id="translation-fold">{'Fold'|t}</span>
32 <span id="translation-fold-all">{'Fold all'|t}</span>
33 <span id="translation-expand">{'Expand'|t}</span>
34 <span id="translation-expand-all">{'Expand all'|t}</span>
35 <span id="translation-delete-link">{'Are you sure you want to delete this link?'|t}</span>
36 <span id="translation-shaarli-desc">
37 {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t}
38 </span>
39</div>
40
30<script src="js/shaarli.js?v={$version_hash}"></script> 41<script src="js/shaarli.js?v={$version_hash}"></script>
31<script src="inc/awesomplete.js?v={$version_hash}#"></script> 42<script src="inc/awesomplete.js?v={$version_hash}#"></script>
32<script src="inc/awesomplete-multiple-tags.js?v={$version_hash}#"></script> 43<script src="inc/awesomplete-multiple-tags.js?v={$version_hash}#"></script>
diff --git a/tpl/default/pluginsadmin.html b/tpl/default/pluginsadmin.html
index 5cc1802f..717cb517 100644
--- a/tpl/default/pluginsadmin.html
+++ b/tpl/default/pluginsadmin.html
@@ -116,8 +116,8 @@
116 </section> 116 </section>
117 117
118 <div class="center more"> 118 <div class="center more">
119 More plugins available 119 {"More plugins available"|t}
120 <a href="doc/Community-&-Related-software.html#third-party-plugins">in the documentation</a>. 120 <a href="doc/Community-&-Related-software.html#third-party-plugins">{"in the documentation"|t}</a>.
121 </div> 121 </div>
122 <div class="center"> 122 <div class="center">
123 <input type="submit" value="{'Save'|t}" name="save"> 123 <input type="submit" value="{'Save'|t}" name="save">