Merge pull request #871 from ArthurHoaro/feature/translation

Shaarli's translation
This commit is contained in:
ArthurHoaro 2017-10-22 13:19:51 +02:00 committed by GitHub
commit d8acf85504
68 changed files with 2744 additions and 280 deletions

1
.gitattributes vendored
View file

@ -22,6 +22,7 @@ Dockerfile text
*.ttf binary *.ttf binary
*.min.css binary *.min.css binary
*.min.js binary *.min.js binary
*.mo binary
# Exclude from Git archives # Exclude from Git archives
.editorconfig export-ignore .editorconfig export-ignore

1
.gitignore vendored
View file

@ -18,6 +18,7 @@ vendor/
# Release archives # Release archives
*.tar.gz *.tar.gz
*.zip *.zip
inc/languages/*/LC_MESSAGES/shaarli.mo
# Development and test resources # Development and test resources
coverage coverage

View file

@ -13,6 +13,8 @@ install:
- composer self-update - composer self-update
- composer install --prefer-dist - composer install --prefer-dist
- locale -a - locale -a
before_script:
- PATH=${PATH//:\.\/node_modules\/\.bin/}
script: script:
- make clean - make clean
- make check_permissions - make check_permissions

View file

@ -130,12 +130,12 @@ check_permissions:
# See phpunit.xml for configuration # See phpunit.xml for configuration
# https://phpunit.de/manual/current/en/appendixes.configuration.html # https://phpunit.de/manual/current/en/appendixes.configuration.html
## ##
test: test: translate
@echo "-------" @echo "-------"
@echo "PHPUNIT" @echo "PHPUNIT"
@echo "-------" @echo "-------"
@mkdir -p sandbox coverage @mkdir -p sandbox coverage
@$(BIN)/phpunit --coverage-php coverage/main.cov --testsuite unit-tests @$(BIN)/phpunit --coverage-php coverage/main.cov --bootstrap tests/bootstrap.php --testsuite unit-tests
locale_test_%: locale_test_%:
@UT_LOCALE=$*.utf8 \ @UT_LOCALE=$*.utf8 \
@ -168,15 +168,15 @@ composer_dependencies: clean
composer install --no-dev --prefer-dist composer install --no-dev --prefer-dist
find vendor/ -name ".git" -type d -exec rm -rf {} + find vendor/ -name ".git" -type d -exec rm -rf {} +
### generate a release tarball and include 3rd-party dependencies ### generate a release tarball and include 3rd-party dependencies and translations
release_tar: composer_dependencies htmldoc release_tar: composer_dependencies htmldoc translate
git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).tar HEAD git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).tar HEAD
tar rvf $(ARCHIVE_VERSION).tar --transform "s|^vendor|$(ARCHIVE_PREFIX)vendor|" vendor/ tar rvf $(ARCHIVE_VERSION).tar --transform "s|^vendor|$(ARCHIVE_PREFIX)vendor|" vendor/
tar rvf $(ARCHIVE_VERSION).tar --transform "s|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/ tar rvf $(ARCHIVE_VERSION).tar --transform "s|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/
gzip $(ARCHIVE_VERSION).tar gzip $(ARCHIVE_VERSION).tar
### generate a release zip and include 3rd-party dependencies ### generate a release zip and include 3rd-party dependencies and translations
release_zip: composer_dependencies htmldoc release_zip: composer_dependencies htmldoc translate
git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD
mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor} mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor}
rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/ rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/
@ -213,3 +213,8 @@ htmldoc:
mkdocs build' mkdocs build'
find doc/html/ -type f -exec chmod a-x '{}' \; find doc/html/ -type f -exec chmod a-x '{}' \;
rm -r venv rm -r venv
### Generate Shaarli's translation compiled file (.mo)
translate:
@find inc/languages/ -name shaarli.po -execdir msgfmt shaarli.po -o shaarli.mo \;

View file

@ -149,12 +149,13 @@ public static function checkUpdate($currentVersion,
public static function checkPHPVersion($minVersion, $curVersion) public static function checkPHPVersion($minVersion, $curVersion)
{ {
if (version_compare($curVersion, $minVersion) < 0) { if (version_compare($curVersion, $minVersion) < 0) {
throw new Exception( $msg = t(
'Your PHP version is obsolete!' 'Your PHP version is obsolete!'
.' Shaarli requires at least PHP '.$minVersion.', and thus cannot run.' . ' Shaarli requires at least PHP %s, and thus cannot run.'
.' Your PHP version has known security vulnerabilities and should be' . ' Your PHP version has known security vulnerabilities and should be'
.' updated as soon as possible.' . ' updated as soon as possible.'
); );
throw new Exception(sprintf($msg, $minVersion));
} }
} }
@ -179,7 +180,7 @@ public static function checkResourcePermissions($conf)
$rainTplDir.'/'.$conf->get('resource.theme'), $rainTplDir.'/'.$conf->get('resource.theme'),
) as $path) { ) as $path) {
if (! is_readable(realpath($path))) { if (! is_readable(realpath($path))) {
$errors[] = '"'.$path.'" directory is not readable'; $errors[] = '"'.$path.'" '. t('directory is not readable');
} }
} }
@ -191,10 +192,10 @@ public static function checkResourcePermissions($conf)
$conf->get('resource.raintpl_tmp'), $conf->get('resource.raintpl_tmp'),
) as $path) { ) as $path) {
if (! is_readable(realpath($path))) { if (! is_readable(realpath($path))) {
$errors[] = '"'.$path.'" directory is not readable'; $errors[] = '"'.$path.'" '. t('directory is not readable');
} }
if (! is_writable(realpath($path))) { if (! is_writable(realpath($path))) {
$errors[] = '"'.$path.'" directory is not writable'; $errors[] = '"'.$path.'" '. t('directory is not writable');
} }
} }
@ -212,10 +213,10 @@ public static function checkResourcePermissions($conf)
} }
if (! is_readable(realpath($path))) { if (! is_readable(realpath($path))) {
$errors[] = '"'.$path.'" file is not readable'; $errors[] = '"'.$path.'" '. t('file is not readable');
} }
if (! is_writable(realpath($path))) { if (! is_writable(realpath($path))) {
$errors[] = '"'.$path.'" file is not writable'; $errors[] = '"'.$path.'" '. t('file is not writable');
} }
} }

View file

@ -13,7 +13,7 @@
function purgeCachedPages($pageCacheDir) function purgeCachedPages($pageCacheDir)
{ {
if (! is_dir($pageCacheDir)) { if (! is_dir($pageCacheDir)) {
$error = 'Cannot purge '.$pageCacheDir.': no directory'; $error = sprintf(t('Cannot purge %s: no directory'), $pageCacheDir);
error_log($error); error_log($error);
return $error; return $error;
} }

View file

@ -148,9 +148,9 @@ protected function buildItem($link, $pageaddr)
$link['url'] = $pageaddr . $link['url']; $link['url'] = $pageaddr . $link['url'];
} }
if ($this->usePermalinks === true) { if ($this->usePermalinks === true) {
$permalink = '<a href="'. $link['url'] .'" title="Direct link">Direct link</a>'; $permalink = '<a href="'. $link['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
} else { } else {
$permalink = '<a href="'. $link['guid'] .'" title="Permalink">Permalink</a>'; $permalink = '<a href="'. $link['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>';
} }
$link['description'] = format_description($link['description'], '', $pageaddr); $link['description'] = format_description($link['description'], '', $pageaddr);
$link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink; $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink;

View file

@ -171,7 +171,7 @@ protected function check()
} }
if (! is_writable($this->historyFilePath)) { if (! is_writable($this->historyFilePath)) {
throw new Exception('History file isn\'t readable or writable'); throw new Exception(t('History file isn\'t readable or writable'));
} }
} }
@ -182,7 +182,7 @@ protected function read()
{ {
$this->history = FileUtils::readFlatDB($this->historyFilePath, []); $this->history = FileUtils::readFlatDB($this->historyFilePath, []);
if ($this->history === false) { if ($this->history === false) {
throw new Exception('Could not parse history file'); throw new Exception(t('Could not parse history file'));
} }
} }

View file

@ -1,21 +1,164 @@
<?php <?php
namespace Shaarli;
use Gettext\GettextTranslator;
use Gettext\Merge;
use Gettext\Translations;
use Gettext\Translator;
use Gettext\TranslatorInterface;
use Shaarli\Config\ConfigManager;
/** /**
* Wrapper function for translation which match the API * Class Languages
* of gettext()/_() and ngettext().
* *
* Not doing translation for now. * Load Shaarli translations using 'gettext/gettext'.
* This class allows to either use PHP gettext extension, or a PHP implementation of gettext,
* with a fixed language, or dynamically using autoLocale().
* *
* @param string $text Text to translate. * Translation files PO/MO files follow gettext standard and must be placed under:
* @param string $nText The plural message ID. * <translation path>/<language>/LC_MESSAGES/<domain>.[po|mo]
* @param int $nb The number of items for plural forms.
* *
* @return String Text translated. * Pros/cons:
* - gettext extension is faster
* - gettext is very system dependent (PHP extension, the locale must be installed, and web server reloaded)
*
* Settings:
* - translation.mode:
* - auto: use default setting (PHP implementation)
* - php: use PHP implementation
* - gettext: use gettext wrapper
* - translation.language:
* - auto: use autoLocale() and the language change according to user HTTP headers
* - fixed language: e.g. 'fr'
* - translation.extensions:
* - domain => translation_path: allow plugins and themes to extend the defaut extension
* The domain must be unique, and translation path must be relative, and contains the tree mentioned above.
*
* @package Shaarli
*/ */
function t($text, $nText = '', $nb = 0) { class Languages
if (empty($nText)) { {
return $text; /**
* Core translations domain
*/
const DEFAULT_DOMAIN = 'shaarli';
/**
* @var TranslatorInterface
*/
protected $translator;
/**
* @var string
*/
protected $language;
/**
* @var ConfigManager
*/
protected $conf;
/**
* Languages constructor.
*
* @param string $language lang determined by autoLocale(), can be overridden.
* @param ConfigManager $conf instance.
*/
public function __construct($language, $conf)
{
$this->conf = $conf;
$confLanguage = $this->conf->get('translation.language', 'auto');
if ($confLanguage === 'auto' || ! $this->isValidLanguage($confLanguage)) {
$this->language = substr($language, 0, 5);
} else {
$this->language = $confLanguage;
}
if (! extension_loaded('gettext')
|| in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php'])
) {
$this->initPhpTranslator();
} else {
$this->initGettextTranslator();
}
// Register default functions (e.g. '__()') to use our Translator
$this->translator->register();
}
/**
* Initialize the translator using php gettext extension (gettext dependency act as a wrapper).
*/
protected function initGettextTranslator ()
{
$this->translator = new GettextTranslator();
$this->translator->setLanguage($this->language);
$this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages');
foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
if ($domain !== self::DEFAULT_DOMAIN) {
$this->translator->loadDomain($domain, $translationPath, false);
}
}
}
/**
* Initialize the translator using a PHP implementation of gettext.
*
* Note that if language po file doesn't exist, errors are ignored (e.g. not installed language).
*/
protected function initPhpTranslator()
{
$this->translator = new Translator();
$translations = new Translations();
// Core translations
try {
/** @var Translations $translations */
$translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po');
$translations->setDomain('shaarli');
$this->translator->loadTranslations($translations);
} catch (\InvalidArgumentException $e) {}
// Extension translations (plugins, themes, etc.).
foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
if ($domain === self::DEFAULT_DOMAIN) {
continue;
}
try {
/** @var Translations $extension */
$extension = Translations::fromPoFile($translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po');
$extension->setDomain($domain);
$this->translator->loadTranslations($extension);
} catch (\InvalidArgumentException $e) {}
}
}
/**
* Checks if a language string is valid.
*
* @param string $language e.g. 'fr' or 'en_US'
*
* @return bool true if valid, false otherwise
*/
protected function isValidLanguage($language)
{
return preg_match('/^[a-z]{2}(_[A-Z]{2})?/', $language) === 1;
}
/**
* Get the list of available languages for Shaarli.
*
* @return array List of available languages, with their label.
*/
public static function getAvailableLanguages()
{
return [
'auto' => t('Automatic'),
'en' => t('English'),
'fr' => t('French'),
];
} }
$actualForm = $nb > 1 ? $nText : $text;
return sprintf($actualForm, $nb);
} }

View file

@ -133,16 +133,16 @@ public function offsetSet($offset, $value)
{ {
// TODO: use exceptions instead of "die" // TODO: use exceptions instead of "die"
if (!$this->loggedIn) { if (!$this->loggedIn) {
die('You are not authorized to add a link.'); die(t('You are not authorized to add a link.'));
} }
if (!isset($value['id']) || empty($value['url'])) { if (!isset($value['id']) || empty($value['url'])) {
die('Internal Error: A link should always have an id and URL.'); die(t('Internal Error: A link should always have an id and URL.'));
} }
if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) { if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) {
die('You must specify an integer as a key.'); die(t('You must specify an integer as a key.'));
} }
if ($offset !== null && $offset !== $value['id']) { if ($offset !== null && $offset !== $value['id']) {
die('Array offset and link ID must be equal.'); die(t('Array offset and link ID must be equal.'));
} }
// If the link exists, we reuse the real offset, otherwise new entry // If the link exists, we reuse the real offset, otherwise new entry
@ -248,13 +248,13 @@ private function check()
$this->links = array(); $this->links = array();
$link = array( $link = array(
'id' => 1, 'id' => 1,
'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', 'title'=> t('The personal, minimalist, super-fast, database free, bookmarking service'),
'url'=>'https://shaarli.readthedocs.io', 'url'=>'https://shaarli.readthedocs.io',
'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login. 'description'=>t('Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page. To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page.
You use the community supported version of the original Shaarli project, by Sebastien Sauvage.', You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'),
'private'=>0, 'private'=>0,
'created'=> new DateTime(), 'created'=> new DateTime(),
'tags'=>'opensource software' 'tags'=>'opensource software'
@ -264,9 +264,9 @@ private function check()
$link = array( $link = array(
'id' => 0, 'id' => 0,
'title'=>'My secret stuff... - Pastebin.com', 'title'=> t('My secret stuff... - Pastebin.com'),
'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.', 'description'=> t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'),
'private'=>1, 'private'=>1,
'created'=> new DateTime('1 minute ago'), 'created'=> new DateTime('1 minute ago'),
'tags'=>'secretstuff', 'tags'=>'secretstuff',

View file

@ -444,5 +444,11 @@ public static function tagsStrToArray($tags, $casesensitive)
class LinkNotFoundException extends Exception class LinkNotFoundException extends Exception
{ {
protected $message = 'The link you are trying to reach does not exist or has been deleted.'; /**
* LinkNotFoundException constructor.
*/
public function __construct()
{
$this->message = t('The link you are trying to reach does not exist or has been deleted.');
}
} }

View file

@ -32,11 +32,10 @@ public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $in
{ {
// see tpl/export.html for possible values // see tpl/export.html for possible values
if (! in_array($selection, array('all', 'public', 'private'))) { if (! in_array($selection, array('all', 'public', 'private'))) {
throw new Exception('Invalid export selection: "'.$selection.'"'); throw new Exception(t('Invalid export selection:') .' "'.$selection.'"');
} }
$bookmarkLinks = array(); $bookmarkLinks = array();
foreach ($linkDb as $link) { foreach ($linkDb as $link) {
if ($link['private'] != 0 && $selection == 'public') { if ($link['private'] != 0 && $selection == 'public') {
continue; continue;
@ -79,14 +78,14 @@ private static function importStatus(
$duration=0 $duration=0
) )
{ {
$status = 'File '.$filename.' ('.$filesize.' bytes) '; $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) { if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
$status .= 'has an unknown file format. Nothing was imported.'; $status .= t('has an unknown file format. Nothing was imported.');
} else { } else {
$status .= 'was successfully processed in '. $duration .' seconds: '; $status .= vsprintf(
$status .= $importCount.' links imported, '; t('was successfully processed in %d seconds: %d links imported, %d links overwritten, %d links skipped.'),
$status .= $overwriteCount.' links overwritten, '; [$duration, $importCount, $overwriteCount, $skipCount]
$status .= $skipCount.' links skipped.'; );
} }
return $status; return $status;
} }

View file

@ -159,9 +159,12 @@ public function renderPage($page)
* *
* @param string $message A messate to display what is not found * @param string $message A messate to display what is not found
*/ */
public function render404($message = 'The page you are trying to reach does not exist or has been deleted.') public function render404($message = '')
{ {
header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); if (empty($message)) {
$message = t('The page you are trying to reach does not exist or has been deleted.');
}
header($_SERVER['SERVER_PROTOCOL'] .' '. t('404 Not Found'));
$this->tpl->assign('error_message', $message); $this->tpl->assign('error_message', $message);
$this->renderPage('404'); $this->renderPage('404');
} }

View file

@ -188,6 +188,9 @@ public function getPluginsMeta()
$metaData[$plugin] = parse_ini_file($metaFile); $metaData[$plugin] = parse_ini_file($metaFile);
$metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins); $metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins);
if (isset($metaData[$plugin]['description'])) {
$metaData[$plugin]['description'] = t($metaData[$plugin]['description']);
}
// Read parameters and format them into an array. // Read parameters and format them into an array.
if (isset($metaData[$plugin]['parameters'])) { if (isset($metaData[$plugin]['parameters'])) {
$params = explode(';', $metaData[$plugin]['parameters']); $params = explode(';', $metaData[$plugin]['parameters']);
@ -203,7 +206,7 @@ public function getPluginsMeta()
$metaData[$plugin]['parameters'][$param]['value'] = ''; $metaData[$plugin]['parameters'][$param]['value'] = '';
// Optional parameter description in parameter.PARAM_NAME= // Optional parameter description in parameter.PARAM_NAME=
if (isset($metaData[$plugin]['parameter.'. $param])) { if (isset($metaData[$plugin]['parameter.'. $param])) {
$metaData[$plugin]['parameters'][$param]['desc'] = $metaData[$plugin]['parameter.'. $param]; $metaData[$plugin]['parameters'][$param]['desc'] = t($metaData[$plugin]['parameter.'. $param]);
} }
} }
} }
@ -237,6 +240,6 @@ class PluginFileNotFoundException extends Exception
*/ */
public function __construct($pluginName) public function __construct($pluginName)
{ {
$this->message = 'Plugin "'. $pluginName .'" files not found.'; $this->message = sprintf(t('Plugin "%s" files not found.'), $pluginName);
} }
} }

View file

@ -73,7 +73,7 @@ public function update()
} }
if ($this->methods === null) { if ($this->methods === null) {
throw new UpdaterException('Couldn\'t retrieve Updater class methods.'); throw new UpdaterException(t('Couldn\'t retrieve Updater class methods.'));
} }
foreach ($this->methods as $method) { foreach ($this->methods as $method) {
@ -482,7 +482,7 @@ private function buildMessage($message)
} }
if (! empty($this->method)) { if (! empty($this->method)) {
$out .= 'An error occurred while running the update '. $this->method . PHP_EOL; $out .= t('An error occurred while running the update ') . $this->method . PHP_EOL;
} }
if (! empty($this->previous)) { if (! empty($this->previous)) {
@ -522,11 +522,11 @@ function read_updates_file($updatesFilepath)
function write_updates_file($updatesFilepath, $updates) function write_updates_file($updatesFilepath, $updates)
{ {
if (empty($updatesFilepath)) { if (empty($updatesFilepath)) {
throw new Exception('Updates file path is not set, can\'t write updates.'); throw new Exception(t('Updates file path is not set, can\'t write updates.'));
} }
$res = file_put_contents($updatesFilepath, implode(';', $updates)); $res = file_put_contents($updatesFilepath, implode(';', $updates));
if ($res === false) { if ($res === false) {
throw new Exception('Unable to write updates in '. $updatesFilepath . '.'); throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.'));
} }
} }

View file

@ -452,7 +452,7 @@ function get_max_upload_size($limitPost, $limitUpload, $format = true)
*/ */
function alphabetical_sort(&$data, $reverse = false, $byKeys = false) function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
{ {
$callback = function($a, $b) use ($reverse) { $callback = function ($a, $b) use ($reverse) {
// Collator is part of PHP intl. // Collator is part of PHP intl.
if (class_exists('Collator')) { if (class_exists('Collator')) {
$collator = new Collator(setlocale(LC_COLLATE, 0)); $collator = new Collator(setlocale(LC_COLLATE, 0));
@ -470,3 +470,18 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
usort($data, $callback); usort($data, $callback);
} }
} }
/**
* Wrapper function for translation which match the API
* of gettext()/_() and ngettext().
*
* @param string $text Text to translate.
* @param string $nText The plural message ID.
* @param int $nb The number of items for plural forms.
* @param string $domain The domain where the translation is stored (default: shaarli).
*
* @return string Text translated.
*/
function t($text, $nText = '', $nb = 1, $domain = 'shaarli') {
return dn__($domain, $text, $nText, $nb);
}

View file

@ -22,10 +22,15 @@ public function read($filepath)
$data = json_decode($data, true); $data = json_decode($data, true);
if ($data === null) { if ($data === null) {
$errorCode = json_last_error(); $errorCode = json_last_error();
$error = 'An error occurred while parsing JSON configuration file ('. $filepath .'): error code #'; $error = sprintf(
$error .= $errorCode. '<br>➜ <code>' . json_last_error_msg() .'</code>'; 'An error occurred while parsing JSON configuration file (%s): error code #%d',
$filepath,
$errorCode
);
$error .= '<br>➜ <code>' . json_last_error_msg() .'</code>';
if ($errorCode === JSON_ERROR_SYNTAX) { if ($errorCode === JSON_ERROR_SYNTAX) {
$error .= '<br>Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as '; $error .= '<br>';
$error .= 'Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as ';
$error .= '<a href="http://jsonlint.com/">jsonlint.com</a>.'; $error .= '<a href="http://jsonlint.com/">jsonlint.com</a>.';
} }
throw new \Exception($error); throw new \Exception($error);
@ -44,8 +49,8 @@ public function write($filepath, $conf)
if (!file_put_contents($filepath, $data)) { if (!file_put_contents($filepath, $data)) {
throw new \IOException( throw new \IOException(
$filepath, $filepath,
'Shaarli could not create the config file. t('Shaarli could not create the config file. '.
Please make sure Shaarli has the right to write in the folder is it installed in.' 'Please make sure Shaarli has the right to write in the folder is it installed in.')
); );
} }
} }

View file

@ -132,7 +132,7 @@ public function get($setting, $default = '')
public function set($setting, $value, $write = false, $isLoggedIn = false) public function set($setting, $value, $write = false, $isLoggedIn = false)
{ {
if (empty($setting) || ! is_string($setting)) { if (empty($setting) || ! is_string($setting)) {
throw new \Exception('Invalid setting key parameter. String expected, got: '. gettype($setting)); throw new \Exception(t('Invalid setting key parameter. String expected, got: '). gettype($setting));
} }
// During the ConfigIO transition, map legacy settings to the new ones. // During the ConfigIO transition, map legacy settings to the new ones.
@ -339,6 +339,10 @@ protected function setDefaultValues()
$this->setEmpty('redirector.url', ''); $this->setEmpty('redirector.url', '');
$this->setEmpty('redirector.encode_url', true); $this->setEmpty('redirector.encode_url', true);
$this->setEmpty('translation.language', 'auto');
$this->setEmpty('translation.mode', 'php');
$this->setEmpty('translation.extensions', []);
$this->setEmpty('plugins', array()); $this->setEmpty('plugins', array());
} }

View file

@ -118,8 +118,8 @@ public function write($filepath, $conf)
) { ) {
throw new \IOException( throw new \IOException(
$filepath, $filepath,
'Shaarli could not create the config file. t('Shaarli could not create the config file. '.
Please make sure Shaarli has the right to write in the folder is it installed in.' 'Please make sure Shaarli has the right to write in the folder is it installed in.')
); );
} }
} }

View file

@ -18,6 +18,6 @@ class MissingFieldConfigException extends \Exception
public function __construct($field) public function __construct($field)
{ {
$this->field = $field; $this->field = $field;
$this->message = 'Configuration value is required for '. $this->field; $this->message = sprintf(t('Configuration value is required for %s'), $this->field);
} }
} }

View file

@ -12,6 +12,6 @@ class PluginConfigOrderException extends \Exception
*/ */
public function __construct() public function __construct()
{ {
$this->message = 'An error occurred while trying to save plugins loading order.'; $this->message = t('An error occurred while trying to save plugins loading order.');
} }
} }

View file

@ -13,6 +13,6 @@ class UnauthorizedConfigException extends \Exception
*/ */
public function __construct() public function __construct()
{ {
$this->message = 'You are not authorized to alter config.'; $this->message = t('You are not authorized to alter config.');
} }
} }

View file

@ -16,7 +16,7 @@ class IOException extends Exception
public function __construct($path, $message = '') public function __construct($path, $message = '')
{ {
$this->path = $path; $this->path = $path;
$this->message = empty($message) ? 'Error accessing' : $message; $this->message = empty($message) ? t('Error accessing') : $message;
$this->message .= ' "' . $this->path .'"'; $this->message .= ' "' . $this->path .'"';
} }
} }

View file

@ -19,7 +19,8 @@
"shaarli/netscape-bookmark-parser": "^2.0", "shaarli/netscape-bookmark-parser": "^2.0",
"erusev/parsedown": "1.6", "erusev/parsedown": "1.6",
"slim/slim": "^3.0", "slim/slim": "^3.0",
"pubsubhubbub/publisher": "dev-master" "pubsubhubbub/publisher": "dev-master",
"gettext/gettext": "^4.4"
}, },
"require-dev": { "require-dev": {
"phpmd/phpmd" : "@stable", "phpmd/phpmd" : "@stable",

249
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "68beedbfa104c788029b079800cfd6e8", "content-hash": "13b7e1e474fe9264b098ba86face0feb",
"packages": [ "packages": [
{ {
"name": "container-interop/container-interop", "name": "container-interop/container-interop",
@ -76,6 +76,129 @@
], ],
"time": "2015-10-04T16:44:32+00:00" "time": "2015-10-04T16:44:32+00:00"
}, },
{
"name": "gettext/gettext",
"version": "v4.4.3",
"source": {
"type": "git",
"url": "https://github.com/oscarotero/Gettext.git",
"reference": "4f57f004635cc6311a20815ebfdc0757cb337113"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/oscarotero/Gettext/zipball/4f57f004635cc6311a20815ebfdc0757cb337113",
"reference": "4f57f004635cc6311a20815ebfdc0757cb337113",
"shasum": ""
},
"require": {
"gettext/languages": "^2.3",
"php": ">=5.4.0"
},
"require-dev": {
"illuminate/view": "*",
"phpunit/phpunit": "^4.8|^5.7",
"squizlabs/php_codesniffer": "^3.0",
"symfony/yaml": "~2",
"twig/extensions": "*",
"twig/twig": "^1.31|^2.0"
},
"suggest": {
"illuminate/view": "Is necessary if you want to use the Blade extractor",
"symfony/yaml": "Is necessary if you want to use the Yaml extractor/generator",
"twig/extensions": "Is necessary if you want to use the Twig extractor",
"twig/twig": "Is necessary if you want to use the Twig extractor"
},
"type": "library",
"autoload": {
"psr-4": {
"Gettext\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oscar Otero",
"email": "oom@oscarotero.com",
"homepage": "http://oscarotero.com",
"role": "Developer"
}
],
"description": "PHP gettext manager",
"homepage": "https://github.com/oscarotero/Gettext",
"keywords": [
"JS",
"gettext",
"i18n",
"mo",
"po",
"translation"
],
"time": "2017-08-09T16:59:46+00:00"
},
{
"name": "gettext/languages",
"version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/mlocati/cldr-to-gettext-plural-rules.git",
"reference": "49c39e51569963cc917a924b489e7025bfb9d8c7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mlocati/cldr-to-gettext-plural-rules/zipball/49c39e51569963cc917a924b489e7025bfb9d8c7",
"reference": "49c39e51569963cc917a924b489e7025bfb9d8c7",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": "^4"
},
"bin": [
"bin/export-plural-rules",
"bin/export-plural-rules.php"
],
"type": "library",
"autoload": {
"psr-4": {
"Gettext\\Languages\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michele Locati",
"email": "mlocati@gmail.com",
"role": "Developer"
}
],
"description": "gettext languages with plural rules",
"homepage": "https://github.com/mlocati/cldr-to-gettext-plural-rules",
"keywords": [
"cldr",
"i18n",
"internationalization",
"l10n",
"language",
"languages",
"localization",
"php",
"plural",
"plural rules",
"plurals",
"translate",
"translations",
"unicode"
],
"time": "2017-03-23T17:02:28+00:00"
},
{ {
"name": "katzgrau/klogger", "name": "katzgrau/klogger",
"version": "1.2.1", "version": "1.2.1",
@ -371,12 +494,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/pubsubhubbub/php-publisher.git", "url": "https://github.com/pubsubhubbub/php-publisher.git",
"reference": "a5d6a0e1cc9d49101c3904480e5b06cbb8addba7" "reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/a5d6a0e1cc9d49101c3904480e5b06cbb8addba7", "url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/0d224daebd504ab61c22fee4db58f8d1fc18945f",
"reference": "a5d6a0e1cc9d49101c3904480e5b06cbb8addba7", "reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -406,7 +529,7 @@
"publishers", "publishers",
"pubsubhubbub" "pubsubhubbub"
], ],
"time": "2016-11-15T06:24:01+00:00" "time": "2017-10-08T10:59:41+00:00"
}, },
{ {
"name": "shaarli/netscape-bookmark-parser", "name": "shaarli/netscape-bookmark-parser",
@ -632,16 +755,16 @@
}, },
{ {
"name": "phpdocumentor/reflection-common", "name": "phpdocumentor/reflection-common",
"version": "1.0", "version": "1.0.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git", "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
"reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
"reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -682,20 +805,20 @@
"reflection", "reflection",
"static analysis" "static analysis"
], ],
"time": "2015-12-27T11:43:31+00:00" "time": "2017-09-11T18:02:19+00:00"
}, },
{ {
"name": "phpdocumentor/reflection-docblock", "name": "phpdocumentor/reflection-docblock",
"version": "3.2.1", "version": "3.2.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
"reference": "183824db76118b9dddffc7e522b91fa175f75119" "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/183824db76118b9dddffc7e522b91fa175f75119", "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157",
"reference": "183824db76118b9dddffc7e522b91fa175f75119", "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -727,7 +850,7 @@
} }
], ],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"time": "2017-08-04T20:55:59+00:00" "time": "2017-08-08T06:39:58+00:00"
}, },
{ {
"name": "phpdocumentor/type-resolver", "name": "phpdocumentor/type-resolver",
@ -844,22 +967,22 @@
}, },
{ {
"name": "phpspec/prophecy", "name": "phpspec/prophecy",
"version": "v1.7.0", "version": "v1.7.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpspec/prophecy.git", "url": "https://github.com/phpspec/prophecy.git",
"reference": "93d39f1f7f9326d746203c7c056f300f7f126073" "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073", "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
"reference": "93d39f1f7f9326d746203c7c056f300f7f126073", "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"doctrine/instantiator": "^1.0.2", "doctrine/instantiator": "^1.0.2",
"php": "^5.3|^7.0", "php": "^5.3|^7.0",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2", "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
"sebastian/comparator": "^1.1|^2.0", "sebastian/comparator": "^1.1|^2.0",
"sebastian/recursion-context": "^1.0|^2.0|^3.0" "sebastian/recursion-context": "^1.0|^2.0|^3.0"
}, },
@ -870,7 +993,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.6.x-dev" "dev-master": "1.7.x-dev"
} }
}, },
"autoload": { "autoload": {
@ -903,7 +1026,7 @@
"spy", "spy",
"stub" "stub"
], ],
"time": "2017-03-02T20:05:34+00:00" "time": "2017-09-04T11:05:03+00:00"
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
@ -1875,20 +1998,20 @@
}, },
{ {
"name": "symfony/config", "name": "symfony/config",
"version": "v3.3.6", "version": "v3.3.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/config.git", "url": "https://github.com/symfony/config.git",
"reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297" "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/54ee12b0dd60f294132cabae6f5da9573d2e5297", "url": "https://api.github.com/repos/symfony/config/zipball/4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
"reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297", "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=5.5.9", "php": "^5.5.9|>=7.0.8",
"symfony/filesystem": "~2.8|~3.0" "symfony/filesystem": "~2.8|~3.0"
}, },
"conflict": { "conflict": {
@ -1933,20 +2056,20 @@
], ],
"description": "Symfony Config Component", "description": "Symfony Config Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-07-19T07:37:29+00:00" "time": "2017-10-04T18:56:58+00:00"
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v2.8.26", "version": "v2.8.28",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd" "reference": "f81549d2c5fdee8d711c9ab3c7e7362353ea5853"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/32a3c6b3398de5db8ed381f4ef92970c59c2fcdd", "url": "https://api.github.com/repos/symfony/console/zipball/f81549d2c5fdee8d711c9ab3c7e7362353ea5853",
"reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd", "reference": "f81549d2c5fdee8d711c9ab3c7e7362353ea5853",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1994,7 +2117,7 @@
], ],
"description": "Symfony Console Component", "description": "Symfony Console Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-07-29T21:26:04+00:00" "time": "2017-10-01T21:00:16+00:00"
}, },
{ {
"name": "symfony/debug", "name": "symfony/debug",
@ -2055,20 +2178,20 @@
}, },
{ {
"name": "symfony/dependency-injection", "name": "symfony/dependency-injection",
"version": "v3.3.6", "version": "v3.3.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/dependency-injection.git", "url": "https://github.com/symfony/dependency-injection.git",
"reference": "8d70987f991481e809c63681ffe8ce3f3fde68a0" "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8d70987f991481e809c63681ffe8ce3f3fde68a0", "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8ebad929aee3ca185b05f55d9cc5521670821ad1",
"reference": "8d70987f991481e809c63681ffe8ce3f3fde68a0", "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=5.5.9", "php": "^5.5.9|>=7.0.8",
"psr/container": "^1.0" "psr/container": "^1.0"
}, },
"conflict": { "conflict": {
@ -2121,24 +2244,24 @@
], ],
"description": "Symfony DependencyInjection Component", "description": "Symfony DependencyInjection Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-07-28T15:27:31+00:00" "time": "2017-10-04T17:15:30+00:00"
}, },
{ {
"name": "symfony/filesystem", "name": "symfony/filesystem",
"version": "v3.3.6", "version": "v3.3.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/filesystem.git", "url": "https://github.com/symfony/filesystem.git",
"reference": "427987eb4eed764c3b6e38d52a0f87989e010676" "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676", "url": "https://api.github.com/repos/symfony/filesystem/zipball/90bc45abf02ae6b7deb43895c1052cb0038506f1",
"reference": "427987eb4eed764c3b6e38d52a0f87989e010676", "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=5.5.9" "php": "^5.5.9|>=7.0.8"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -2170,24 +2293,24 @@
], ],
"description": "Symfony Filesystem Component", "description": "Symfony Filesystem Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-07-11T07:17:58+00:00" "time": "2017-10-03T13:33:10+00:00"
}, },
{ {
"name": "symfony/finder", "name": "symfony/finder",
"version": "v3.3.6", "version": "v3.3.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/finder.git", "url": "https://github.com/symfony/finder.git",
"reference": "baea7f66d30854ad32988c11a09d7ffd485810c4" "reference": "773e19a491d97926f236942484cb541560ce862d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4", "url": "https://api.github.com/repos/symfony/finder/zipball/773e19a491d97926f236942484cb541560ce862d",
"reference": "baea7f66d30854ad32988c11a09d7ffd485810c4", "reference": "773e19a491d97926f236942484cb541560ce862d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=5.5.9" "php": "^5.5.9|>=7.0.8"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -2219,20 +2342,20 @@
], ],
"description": "Symfony Finder Component", "description": "Symfony Finder Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-06-01T21:01:25+00:00" "time": "2017-10-02T06:42:24+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.4.0", "version": "v1.6.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git", "url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "f29dca382a6485c3cbe6379f0c61230167681937" "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f29dca382a6485c3cbe6379f0c61230167681937", "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
"reference": "f29dca382a6485c3cbe6379f0c61230167681937", "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2244,7 +2367,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.4-dev" "dev-master": "1.6-dev"
} }
}, },
"autoload": { "autoload": {
@ -2278,24 +2401,24 @@
"portable", "portable",
"shim" "shim"
], ],
"time": "2017-06-09T14:24:12+00:00" "time": "2017-10-11T12:05:26+00:00"
}, },
{ {
"name": "symfony/yaml", "name": "symfony/yaml",
"version": "v3.3.6", "version": "v3.3.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/yaml.git", "url": "https://github.com/symfony/yaml.git",
"reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed" "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/ddc23324e6cfe066f3dd34a37ff494fa80b617ed", "url": "https://api.github.com/repos/symfony/yaml/zipball/8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
"reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed", "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=5.5.9" "php": "^5.5.9|>=7.0.8"
}, },
"require-dev": { "require-dev": {
"symfony/console": "~2.8|~3.0" "symfony/console": "~2.8|~3.0"
@ -2333,7 +2456,7 @@
], ],
"description": "Symfony Yaml Component", "description": "Symfony Yaml Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2017-07-23T12:43:26+00:00" "time": "2017-10-05T14:43:42+00:00"
}, },
{ {
"name": "theseer/fdomdocument", "name": "theseer/fdomdocument",

View file

@ -36,6 +36,7 @@ In most cases, download Shaarli from the [releases](https://github.com/shaarli/S
$ mkdir -p /path/to/shaarli && cd /path/to/shaarli/ $ mkdir -p /path/to/shaarli && cd /path/to/shaarli/
$ git clone -b v0.9 https://github.com/shaarli/Shaarli.git . $ git clone -b v0.9 https://github.com/shaarli/Shaarli.git .
$ composer install --no-dev --prefer-dist $ composer install --no-dev --prefer-dist
$ make translate
``` ```
## Stable version ## Stable version
@ -83,13 +84,14 @@ $ git clone https://github.com/shaarli/Shaarli.git -b master /path/to/shaarli/
# install/update third-party dependencies # install/update third-party dependencies
$ cd /path/to/shaarli $ cd /path/to/shaarli
$ composer install --no-dev --prefer-dist $ composer install --no-dev --prefer-dist
$ make translate
``` ```
## Finish Installation ## Finish Installation
Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser. Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser.
![install screenshot](http://i.imgur.com/wuMpDSN.png) ![install screenshot](images/install-shaarli.png)
Setup your Shaarli installation, and it's ready to use! Setup your Shaarli installation, and it's ready to use!

View file

@ -39,3 +39,4 @@ Extension | Required? | Usage
[`php-gd`](http://php.net/manual/en/book.image.php) | optional | thumbnail resizing [`php-gd`](http://php.net/manual/en/book.image.php) | optional | thumbnail resizing
[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`) [`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`)
[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way [`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way
[`php-gettext`](http://php.net/manual/en/book.gettext.php) | optional | Use the translation system in gettext mode (faster)

View file

@ -81,6 +81,20 @@ _These settings should not be edited_
- **page_cache**: Shaarli's internal cache directory. - **page_cache**: Shaarli's internal cache directory.
- **ban_file**: Banned IP file path. - **ban_file**: Banned IP file path.
### Translation
- **language**: translation language (also see [Translations](Translations))
- **auto** (default): The translation language is chosen from the browser locale.
It means that the language can be different for 2 different visitors depending on their locale.
- **en**: Use the English translation.
- **fr**: Use the French translation.
- **mode**:
- **auto** or **php** (default): Use the PHP implementation of gettext (slower)
- **gettext**: Use PHP builtin gettext extension
(faster, but requires `php-gettext` to be installed and to reload the web server on update)
- **extension**: Translation extensions for custom themes or plugins.
Must be an associative array: `translation domain => translation path`.
### Updates ### Updates
- **check_updates**: Enable or disable update check to the git repository. - **check_updates**: Enable or disable update check to the git repository.
@ -211,6 +225,13 @@ _These settings should not be edited_
"plugins": { "plugins": {
"WALLABAG_URL": "http://demo.wallabag.org", "WALLABAG_URL": "http://demo.wallabag.org",
"WALLABAG_VERSION": "1" "WALLABAG_VERSION": "1"
},
"translation": {
"language": "fr",
"mode": "php",
"extensions": {
"demo": "plugins/demo_plugin/languages/"
}
} }
} ?> } ?>
``` ```

152
doc/md/Translations.md Normal file
View file

@ -0,0 +1,152 @@
## Translations
Shaarli supports [gettext](https://www.gnu.org/software/gettext/manual/gettext.html) translations
since `>= v0.9.2`.
Note that only the `default` theme supports translations.
### Contributing
We encourage the community to contribute to Shaarli's translation either by improving existing
translations or submitting a new language.
Contributing to the translation does not require development skill.
Please submit a pull request with the `.po` file updated/created. Note that the compiled file (`.mo`)
is not stored on the repository, and is generated during the release process.
### How to
First, install [Poedit](https://poedit.net/) tool.
Poedit will extract strings to translate from the PHP source code.
**Important**: due to the usage of a template engine, it's important to generate PHP cache files to extract
every translatable string.
You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended)
or visit every template page in your browser to generate cache files, while logged in.
Here is a list :
```
http://<replace_domain>/
http://<replace_domain>/?nonope
http://<replace_domain>/?do=addlink
http://<replace_domain>/?do=changepasswd
http://<replace_domain>/?do=changetag
http://<replace_domain>/?do=configure
http://<replace_domain>/?do=tools
http://<replace_domain>/?do=daily
http://<replace_domain>/?post
http://<replace_domain>/?do=export
http://<replace_domain>/?do=import
http://<replace_domain>/?do=login
http://<replace_domain>/?do=picwall
http://<replace_domain>/?do=pluginadmin
http://<replace_domain>/?do=tagcloud
http://<replace_domain>/?do=taglist
```
#### Improve existing translation
In Poedit, click on "Edit a Translation", and from Shaarli's directory open
`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
The existing list of translatable strings should have been loaded, then click on the "Update" button.
You can start editing the translation.
![poedit-screenshot](images/poedit-1.jpg)
Save when you're done, then you can submit a pull request containing the updated `shaarli.po`.
#### Add a new language
Open Poedit and select "Create New Translation", then from Shaarli's directory open
`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
Then select the language you want to create.
Click on `File > Save as...`, and save your file in `<shaarli directory>/inc/language/<new language>/LC_MESSAGES/shaarli.po`.
`<new language>` here should be the language code respecting the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-2)
format in lowercase (e.g. `de` for German).
Then click on the "Update" button, and you can start to translate every available string.
Save when you're done, then you can submit a pull request containing the new `shaarli.po`.
### Extend Shaarli's translation
If you're writing a custom theme, or a non official plugin, you might want to use the translation system,
but you won't be able to able to override Shaarli's translation.
However, you can add your own translation domain which extends the main translation list.
> Note that you can find a live example of translation extension in the `demo_plugin`.
First, create your translation files tree directory:
```
<your_module>/languages/<ISO 3166-1 alpha-2 language code>/LC_MESSAGES/
```
Your `.po` files must be named like your domain. E.g. if your translation domain is `my_theme`, then your file will be
`my_theme.po`.
Users have to register your extension in their configuration with the parameter
`translation.extensions.<domain>: <translation files path>`.
Example:
```php
if (! $conf->exists('translation.extensions.my_theme')) {
$conf->set('translation.extensions.my_theme', '<your_module>/languages/');
$conf->write(true);
}
```
> Note that the page needs to be reloaded after the registration.
It is then recommended to create a custom translation function which will call the `t()` function with your domain.
For example :
```php
function my_theme_t($text, $nText = '', $nb = 1)
{
return t($text, $nText, $nb, 'my_theme'); // the last parameter is your translation domain.
}
```
All strings which can be translated should be processed through your function:
```php
my_theme_t('Comment');
my_theme_t('Comment', 'Comments', 2);
```
Or in templates:
```php
{'Comment'|my_theme_t}
{function="my_theme_t('Comment', 'Comments', 2)"}
```
> Note than in template, you need to visit your page at least once to generate a cache file.
When you're done, open Poedit and load translation strings from sources:
1. `File > New`
2. Choose your language
3. Save your `PO` file in `<your_module>/languages/<language code>/LC_MESSAGES/my_theme.po`.
4. Go to `Catalog > Properties...`
5. Fill the `Translation Properties` tab
6. Add your source path in the `Sources Paths` tab
7. In the `Sources Keywords` tab uncheck "Also use default keywords" and add the following lines:
```
my_theme_t
my_theme_t:1,2
```
Click on the "Update" button and you're free to start your translations!

View file

@ -39,7 +39,10 @@ We recommend that you use the latest release tarball with the `-full` suffix. It
Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory! Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory!
After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli-configuration) for more details). If you use translations in gettext mode - meaning you manually changed the default mode -,
reload your web server.
After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli configuration) for more details).
## Upgrading with Git ## Upgrading with Git
@ -72,6 +75,14 @@ Updating dependencies
Downloading: 100% Downloading: 100%
``` ```
Shaarli >= `v0.9.2` supports translations:
```bash
$ make translate
```
If you use translations in gettext mode, reload your web server.
### Migrating and upgrading from Sebsauvage's repository ### Migrating and upgrading from Sebsauvage's repository
If you have installed Shaarli from [Sebsauvage's original Git repository](https://github.com/sebsauvage/Shaarli), you can use [Git remotes](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) to update your working copy. If you have installed Shaarli from [Sebsauvage's original Git repository](https://github.com/sebsauvage/Shaarli), you can use [Git remotes](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) to update your working copy.
@ -151,6 +162,14 @@ Updating dependencies
Downloading: 100% Downloading: 100%
``` ```
Shaarli >= `v0.9.2` supports translations:
```bash
$ make translate
```
If you use translations in gettext mode, reload your web server.
Optionally, you can delete information related to the legacy version: Optionally, you can delete information related to the legacy version:
```bash ```bash

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
doc/md/images/poedit-1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

File diff suppressed because it is too large Load diff

View file

@ -64,7 +64,6 @@
require_once 'application/FileUtils.php'; require_once 'application/FileUtils.php';
require_once 'application/History.php'; require_once 'application/History.php';
require_once 'application/HttpUtils.php'; require_once 'application/HttpUtils.php';
require_once 'application/Languages.php';
require_once 'application/LinkDB.php'; require_once 'application/LinkDB.php';
require_once 'application/LinkFilter.php'; require_once 'application/LinkFilter.php';
require_once 'application/LinkUtils.php'; require_once 'application/LinkUtils.php';
@ -76,6 +75,7 @@
require_once 'application/PluginManager.php'; require_once 'application/PluginManager.php';
require_once 'application/Router.php'; require_once 'application/Router.php';
require_once 'application/Updater.php'; require_once 'application/Updater.php';
use \Shaarli\Languages;
use \Shaarli\ThemeUtils; use \Shaarli\ThemeUtils;
use \Shaarli\Config\ConfigManager; use \Shaarli\Config\ConfigManager;
@ -121,8 +121,16 @@
} }
$conf = new ConfigManager(); $conf = new ConfigManager();
// Sniff browser language and set date format accordingly.
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
}
new Languages(setlocale(LC_MESSAGES, 0), $conf);
$conf->setEmpty('general.timezone', date_default_timezone_get()); $conf->setEmpty('general.timezone', date_default_timezone_get());
$conf->setEmpty('general.title', 'Shared links on '. escape(index_url($_SERVER))); $conf->setEmpty('general.title', t('Shared links on '). escape(index_url($_SERVER)));
RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
@ -144,7 +152,7 @@
$errors = ApplicationUtils::checkResourcePermissions($conf); $errors = ApplicationUtils::checkResourcePermissions($conf);
if ($errors != array()) { if ($errors != array()) {
$message = '<p>Insufficient permissions:</p><ul>'; $message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
foreach ($errors as $error) { foreach ($errors as $error) {
$message .= '<li>'.$error.'</li>'; $message .= '<li>'.$error.'</li>';
@ -163,11 +171,6 @@
// a token depending of deployment salt, user password, and the current ip // a token depending of deployment salt, user password, and the current ip
define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt'))); define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt')));
// Sniff browser language and set date format accordingly.
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
}
/** /**
* Checking session state (i.e. is the user still logged in) * Checking session state (i.e. is the user still logged in)
* *
@ -376,7 +379,7 @@ function ban_canLogin($conf)
// Process login form: Check if login/password is correct. // Process login form: Check if login/password is correct.
if (isset($_POST['login'])) if (isset($_POST['login']))
{ {
if (!ban_canLogin($conf)) die('I said: NO. You are banned for the moment. Go away.'); if (!ban_canLogin($conf)) die(t('I said: NO. You are banned for the moment. Go away.'));
if (isset($_POST['password']) if (isset($_POST['password'])
&& tokenOk($_POST['token']) && tokenOk($_POST['token'])
&& (check_auth($_POST['login'], $_POST['password'], $conf)) && (check_auth($_POST['login'], $_POST['password'], $conf))
@ -440,7 +443,8 @@ function ban_canLogin($conf)
} }
} }
} }
echo '<script>alert("Wrong login/password.");document.location=\'?do=login'.$redir.'\';</script>'; // Redirect to login screen. // Redirect to login screen.
echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'?do=login'.$redir.'\';</script>';
exit; exit;
} }
} }
@ -1100,16 +1104,19 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
if ($targetPage == Router::$PAGE_CHANGEPASSWORD) if ($targetPage == Router::$PAGE_CHANGEPASSWORD)
{ {
if ($conf->get('security.open_shaarli')) { if ($conf->get('security.open_shaarli')) {
die('You are not supposed to change a password on an Open Shaarli.'); die(t('You are not supposed to change a password on an Open Shaarli.'));
} }
if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword']))
{ {
if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away! if (!tokenOk($_POST['token'])) die(t('Wrong token.')); // Go away!
// Make sure old password is correct. // Make sure old password is correct.
$oldhash = sha1($_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt')); $oldhash = sha1($_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt'));
if ($oldhash!= $conf->get('credentials.hash')) { echo '<script>alert("The old password is not correct.");document.location=\'?do=changepasswd\';</script>'; exit; } if ($oldhash!= $conf->get('credentials.hash')) {
echo '<script>alert("'. t('The old password is not correct.') .'");document.location=\'?do=changepasswd\';</script>';
exit;
}
// Save new password // Save new password
// Salt renders rainbow-tables attacks useless. // Salt renders rainbow-tables attacks useless.
$conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
@ -1127,7 +1134,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>'; echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>';
exit; exit;
} }
echo '<script>alert("Your password has been changed.");document.location=\'?do=tools\';</script>'; echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'?do=tools\';</script>';
exit; exit;
} }
else // show the change password form. else // show the change password form.
@ -1143,7 +1150,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
if (!empty($_POST['title']) ) if (!empty($_POST['title']) )
{ {
if (!tokenOk($_POST['token'])) { if (!tokenOk($_POST['token'])) {
die('Wrong token.'); // Go away! die(t('Wrong token.')); // Go away!
} }
$tz = 'UTC'; $tz = 'UTC';
if (!empty($_POST['continent']) && !empty($_POST['city']) if (!empty($_POST['continent']) && !empty($_POST['city'])
@ -1163,6 +1170,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
$conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks'])); $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
$conf->set('api.enabled', !empty($_POST['enableApi'])); $conf->set('api.enabled', !empty($_POST['enableApi']));
$conf->set('api.secret', escape($_POST['apiSecret'])); $conf->set('api.secret', escape($_POST['apiSecret']));
$conf->set('translation.language', escape($_POST['language']));
try { try {
$conf->write(isLoggedIn()); $conf->write(isLoggedIn());
$history->updateSettings(); $history->updateSettings();
@ -1178,7 +1187,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>'; echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>';
exit; exit;
} }
echo '<script>alert("Configuration was saved.");document.location=\'?do=configure\';</script>'; echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'?do=configure\';</script>';
exit; exit;
} }
else // Show the configuration form. else // Show the configuration form.
@ -1200,6 +1209,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
$PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false)); $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
$PAGE->assign('api_enabled', $conf->get('api.enabled', true)); $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
$PAGE->assign('api_secret', $conf->get('api.secret')); $PAGE->assign('api_secret', $conf->get('api.secret'));
$PAGE->assign('languages', Languages::getAvailableLanguages());
$PAGE->assign('language', $conf->get('translation.language'));
$PAGE->renderPage('configure'); $PAGE->renderPage('configure');
exit; exit;
} }
@ -1215,7 +1226,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
} }
if (!tokenOk($_POST['token'])) { if (!tokenOk($_POST['token'])) {
die('Wrong token.'); die(t('Wrong token.'));
} }
$alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), escape($_POST['totag'])); $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), escape($_POST['totag']));
@ -1225,9 +1236,10 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
} }
$delete = empty($_POST['totag']); $delete = empty($_POST['totag']);
$redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag'])); $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
$count = count($alteredLinks);
$alert = $delete $alert = $delete
? sprintf(t('The tag was removed from %d links.'), count($alteredLinks)) ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d links.', $count), $count)
: sprintf(t('The tag was renamed in %d links.'), count($alteredLinks)); : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d links.', $count), $count);
echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>'; echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
exit; exit;
} }
@ -1244,7 +1256,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
{ {
// Go away! // Go away!
if (! tokenOk($_POST['token'])) { if (! tokenOk($_POST['token'])) {
die('Wrong token.'); die(t('Wrong token.'));
} }
// lf_id should only be present if the link exists. // lf_id should only be present if the link exists.
@ -1344,7 +1356,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
if ($targetPage == Router::$PAGE_DELETELINK) if ($targetPage == Router::$PAGE_DELETELINK)
{ {
if (! tokenOk($_GET['token'])) { if (! tokenOk($_GET['token'])) {
die('Wrong token.'); die(t('Wrong token.'));
} }
$ids = trim($_GET['lf_linkdate']); $ids = trim($_GET['lf_linkdate']);
@ -1443,7 +1455,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
if ($url == '') { if ($url == '') {
$url = '?' . smallHash($linkdate . $LINKSDB->getNextId()); $url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
$title = $conf->get('general.default_note_title', 'Note: '); $title = $conf->get('general.default_note_title', t('Note: '));
} }
$url = escape($url); $url = escape($url);
$title = escape($title); $title = escape($title);
@ -1550,11 +1562,14 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
// Import bookmarks from an uploaded file // Import bookmarks from an uploaded file
if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) { if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
// The file is too big or some form field may be missing. // The file is too big or some form field may be missing.
echo '<script>alert("The file you are trying to upload is probably' $msg = sprintf(
.' bigger than what this webserver can accept (' t(
.get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')).').' 'The file you are trying to upload is probably bigger than what this webserver can accept'
.' Please upload in smaller chunks.");document.location=\'?do=' .' (%s). Please upload in smaller chunks.'
.Router::$PAGE_IMPORT .'\';</script>'; ),
get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
);
echo '<script>alert("'. $msg .'");document.location=\'?do='.Router::$PAGE_IMPORT .'\';</script>';
exit; exit;
} }
if (! tokenOk($_POST['token'])) { if (! tokenOk($_POST['token'])) {
@ -1962,12 +1977,20 @@ function install($conf)
// (Because on some hosts, session.save_path may not be set correctly, // (Because on some hosts, session.save_path may not be set correctly,
// or we may not have write access to it.) // or we may not have write access to it.)
if (isset($_GET['test_session']) && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) if (isset($_GET['test_session']) && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working'))
{ // Step 2: Check if data in session is correct. {
echo '<pre>Sessions do not seem to work correctly on your server.<br>'; // Step 2: Check if data in session is correct.
echo 'Make sure the variable session.save_path is set correctly in your php config, and that you have write access to it.<br>'; $msg = t(
echo 'It currently points to '.session_save_path().'<br>'; '<pre>Sessions do not seem to work correctly on your server.<br>'.
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>'; 'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
echo '<br><a href="?">Click to try again.</a></pre>'; 'and that you have write access to it.<br>'.
'It currently points to %s.<br>'.
'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>'
);
$msg = sprintf($msg, session_save_path());
echo $msg;
echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
die; die;
} }
if (!isset($_SESSION['session_tested'])) if (!isset($_SESSION['session_tested']))
@ -2000,6 +2023,7 @@ function install($conf)
} else { } else {
$conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER))); $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER)));
} }
$conf->set('translation.language', escape($_POST['language']));
$conf->set('updates.check_updates', !empty($_POST['updateCheck'])); $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
$conf->set('api.enabled', !empty($_POST['enableApi'])); $conf->set('api.enabled', !empty($_POST['enableApi']));
$conf->set( $conf->set(
@ -2031,6 +2055,7 @@ function install($conf)
list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get()); list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
$PAGE->assign('continents', $continents); $PAGE->assign('continents', $continents);
$PAGE->assign('cities', $cities); $PAGE->assign('cities', $cities);
$PAGE->assign('languages', Languages::getAvailableLanguages());
$PAGE->renderPage('install'); $PAGE->renderPage('install');
exit; exit;
} }

View file

@ -43,6 +43,7 @@ pages:
- Versioning and Branches: Versioning-and-Branches.md - Versioning and Branches: Versioning-and-Branches.md
- Security: Security.md - Security: Security.md
- Static analysis: Static-analysis.md - Static analysis: Static-analysis.md
- Translations: Translations.md
- Theming: Theming.md - Theming: Theming.md
- Unit tests: Unit-tests.md - Unit tests: Unit-tests.md
- Unit tests inside Docker: Unit-tests-Docker.md - Unit tests inside Docker: Unit-tests-Docker.md

View file

@ -1,28 +0,0 @@
https://github.com/shaarli/Shaarli/issues/181 - Add Disqus or Isso comments box on a permalink page
* http://posativ.org/isso/
* install debian package https://packages.debian.org/sid/isso
* configure server http://posativ.org/isso/docs/configuration/server/
* configure client http://posativ.org/isso/docs/configuration/client/
* http://posativ.org/isso/docs/quickstart/ and add `<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)
Problem: by default, Isso thread ID is guessed from the current url (only one thread per page).
if we want multiple threads on a single page (shaarli linklist), we must use : the `data-isso-id` client config,
with data-isso-id being the permalink of an item.
`<section data-isso-id="aH7klxW" id="isso-thread"></section>`
`data-isso-id: Set a custom thread id, defaults to current URI.`
Problem: feature is currently broken https://github.com/posativ/isso/issues/27
Another option, only display isso threads when current URL is a permalink (`\?(A-Z|a-z|0-9|-){7}`) (only show thread
when displaying only this link), and just display a "comments" button on each linklist item. Optionally show the comment
count on each item using the API (http://posativ.org/isso/docs/extras/api/#get-comment-count). API requests can be done
by raintpl `{function` or client-side with js. The former should be faster if isso and shaarli are on ther same server.
Showing all full isso threads in the linklist would destroy layout
-----------------------------------------------------------
http://www.git-attitude.fr/2014/11/04/git-rerere/ for the merge

View file

@ -26,11 +26,11 @@ function hook_addlink_toolbar_render_header($data)
array( array(
'type' => 'text', 'type' => 'text',
'name' => 'post', 'name' => 'post',
'placeholder' => 'URI', 'placeholder' => t('URI'),
), ),
array( array(
'type' => 'submit', 'type' => 'submit',
'value' => 'Add link', 'value' => t('Add link'),
'class' => 'bigbutton', 'class' => 'bigbutton',
), ),
), ),
@ -40,3 +40,12 @@ function hook_addlink_toolbar_render_header($data)
return $data; return $data;
} }
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function addlink_toolbar_dummy_translation()
{
// meta
t('Adds the addlink input on the linklist page.');
}

View file

@ -1 +1,5 @@
<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> <span>
<a href="https://web.archive.org/web/%s">
<img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
</a>
</span>

View file

@ -20,9 +20,18 @@ function hook_archiveorg_render_linklist($data)
if($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) { if($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) {
continue; continue;
} }
$archive = sprintf($archive_html, $value['url']); $archive = sprintf($archive_html, $value['url'], t('View on archive.org'));
$value['link_plugin'][] = $archive; $value['link_plugin'][] = $archive;
} }
return $data; return $data;
} }
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function archiveorg_dummy_translation()
{
// meta
t('For each link, add an Archive.org icon.');
}

View file

@ -14,6 +14,26 @@
* and check user status with _LOGGEDIN_. * and check user status with _LOGGEDIN_.
*/ */
use Shaarli\Config\ConfigManager;
/**
* In the footer hook, there is a working example of a translation extension for Shaarli.
*
* The extension must be attached to a new translation domain (i.e. NOT 'shaarli').
* Use case: any custom theme or non official plugin can use the translation system.
*
* See the documentation for more information.
*/
const EXT_TRANSLATION_DOMAIN = 'demo';
/*
* This is not necessary, but it's easier if you don't want Poedit to mix up your translations.
*/
function demo_plugin_t($text, $nText = '', $nb = 1)
{
return t($text, $nText, $nb, EXT_TRANSLATION_DOMAIN);
}
/** /**
* Initialization function. * Initialization function.
* It will be called when the plugin is loaded. * It will be called when the plugin is loaded.
@ -27,6 +47,12 @@ function demo_plugin_init($conf)
{ {
$conf->get('toto', 'nope'); $conf->get('toto', 'nope');
if (! $conf->exists('translation.extensions.demo')) {
// Custom translation with the domain 'demo'
$conf->set('translation.extensions.demo', 'plugins/demo_plugin/languages/');
$conf->write(true);
}
$errors[] = 'This a demo init error.'; $errors[] = 'This a demo init error.';
return $errors; return $errors;
} }
@ -160,7 +186,7 @@ function hook_demo_plugin_render_includes($data)
function hook_demo_plugin_render_footer($data) function hook_demo_plugin_render_footer($data)
{ {
// footer text // footer text
$data['text'][] = 'Shaarli is now enhanced by the awesome demo_plugin.'; $data['text'][] = '<br>'. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.');
// Free elements at the end of the page. // Free elements at the end of the page.
$data['endofpage'][] = '<marquee id="demo_marquee">' . $data['endofpage'][] = '<marquee id="demo_marquee">' .
@ -433,3 +459,12 @@ function hook_demo_plugin_render_feed($data)
} }
return $data; return $data;
} }
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function demo_dummy_translation()
{
// meta
t('A demo plugin covering all use cases for template designers and plugin developers.');
}

Binary file not shown.

View file

@ -0,0 +1,21 @@
msgid ""
msgstr ""
"Project-Id-Version: Demo plugin\n"
"POT-Creation-Date: 2017-08-19 10:45+0200\n"
"PO-Revision-Date: 2017-08-19 11:28+0200\n"
"Last-Translator: \n"
"Language-Team: demo\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.2\n"
"X-Poedit-Basepath: ../../..\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Poedit-KeywordsList: ;demo_plugin_t:1,2;demo_plugin_t\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-SearchPath-0: .\n"
#: demo_plugin.php:173
msgid "Shaarli is now enhanced by the awesome demo_plugin."
msgstr "Shaarli est maintenant amélioré avec le fantastique demo_plugin."

View file

@ -4,10 +4,11 @@
* Plugin Isso. * Plugin Isso.
*/ */
use Shaarli\Config\ConfigManager;
/** /**
* Display an error everywhere if the plugin is enabled without configuration. * Display an error everywhere if the plugin is enabled without configuration.
* *
* @param $data array List of links
* @param $conf ConfigManager instance * @param $conf ConfigManager instance
* *
* @return mixed - linklist data with Isso plugin. * @return mixed - linklist data with Isso plugin.
@ -16,8 +17,8 @@ function isso_init($conf)
{ {
$issoUrl = $conf->get('plugins.ISSO_SERVER'); $issoUrl = $conf->get('plugins.ISSO_SERVER');
if (empty($issoUrl)) { if (empty($issoUrl)) {
$error = 'Isso plugin error: '. $error = t('Isso plugin error: '.
'Please define the "ISSO_SERVER" setting in the plugin administration page.'; 'Please define the "ISSO_SERVER" setting in the plugin administration page.');
return array($error); return array($error);
} }
} }
@ -52,3 +53,13 @@ function hook_isso_render_linklist($data, $conf)
return $data; return $data;
} }
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function isso_dummy_translation()
{
// meta
t('Let visitor comment your shaares on permalinks with Isso.');
t('Isso server URL (without \'http://\')');
}

View file

@ -1,5 +1,5 @@
<div class="md_help"> <div class="md_help">
Description will be rendered with %s
<a href="http://daringfireball.net/projects/markdown/syntax" title="Markdown syntax documentation"> <a href="http://daringfireball.net/projects/markdown/syntax" title="%s">
Markdown syntax</a>. %s</a>.
</div> </div>

View file

@ -154,8 +154,13 @@ function hook_markdown_render_includes($data)
function hook_markdown_render_editlink($data) function hook_markdown_render_editlink($data)
{ {
// Load help HTML into a string // Load help HTML into a string
$data['edit_link_plugin'][] = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html'); $txt = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
$translations = [
t('Description will be rendered with'),
t('Markdown syntax documentation'),
t('Markdown syntax'),
];
$data['edit_link_plugin'][] = vsprintf($txt, $translations);
// Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion. // Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion.
if (! in_array(NO_MD_TAG, $data['tags'])) { if (! in_array(NO_MD_TAG, $data['tags'])) {
$data['tags'][NO_MD_TAG] = 0; $data['tags'][NO_MD_TAG] = 0;
@ -325,3 +330,15 @@ function process_markdown($description, $escape = true, $allowedProtocols = [])
return $processedDescription; return $processedDescription;
} }
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function markdown_dummy_translation()
{
// meta
t('Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
If your shaared descriptions contained HTML tags before enabling the markdown plugin,
enabling it might break your page.
See the <a href="https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering">README</a>.');
}

View file

@ -18,8 +18,8 @@ function piwik_init($conf)
$piwikUrl = $conf->get('plugins.PIWIK_URL'); $piwikUrl = $conf->get('plugins.PIWIK_URL');
$piwikSiteid = $conf->get('plugins.PIWIK_SITEID'); $piwikSiteid = $conf->get('plugins.PIWIK_SITEID');
if (empty($piwikUrl) || empty($piwikSiteid)) { if (empty($piwikUrl) || empty($piwikSiteid)) {
$error = 'Piwik plugin error: ' . $error = t('Piwik plugin error: ' .
'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.'; 'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.');
return array($error); return array($error);
} }
} }
@ -60,3 +60,14 @@ function hook_piwik_render_footer($data, $conf)
return $data; return $data;
} }
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function piwik_dummy_translation()
{
// meta
t('A plugin that adds Piwik tracking code to Shaarli pages.');
t('Piwik URL');
t('Piwik site ID');
}

View file

@ -19,10 +19,10 @@ function hook_playvideos_render_header($data)
$playvideo = array( $playvideo = array(
'attr' => array( 'attr' => array(
'href' => '#', 'href' => '#',
'title' => 'Video player', 'title' => t('Video player'),
'id' => 'playvideos', 'id' => 'playvideos',
), ),
'html' => '► Play Videos' 'html' => '► '. t('Play Videos')
); );
$data['buttons_toolbar'][] = $playvideo; $data['buttons_toolbar'][] = $playvideo;
} }
@ -46,3 +46,12 @@ function hook_playvideos_render_footer($data)
return $data; return $data;
} }
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function playvideos_dummy_translation()
{
// meta
t('Add a button in the toolbar allowing to watch all videos.');
}

View file

@ -10,6 +10,7 @@
*/ */
use pubsubhubbub\publisher\Publisher; use pubsubhubbub\publisher\Publisher;
use Shaarli\Config\ConfigManager;
/** /**
* Plugin init function - set the hub to the default appspot one. * Plugin init function - set the hub to the default appspot one.
@ -65,7 +66,7 @@ function hook_pubsubhubbub_save_link($data, $conf)
$p = new Publisher($conf->get('plugins.PUBSUBHUB_URL')); $p = new Publisher($conf->get('plugins.PUBSUBHUB_URL'));
$p->publish_update($feeds, $httpPost); $p->publish_update($feeds, $httpPost);
} catch (Exception $e) { } catch (Exception $e) {
error_log('Could not publish to PubSubHubbub: ' . $e->getMessage()); error_log(sprintf(t('Could not publish to PubSubHubbub: %s'), $e->getMessage()));
} }
return $data; return $data;
@ -91,11 +92,20 @@ function nocurl_http_post($url, $postString) {
$context = stream_context_create($params); $context = stream_context_create($params);
$fp = @fopen($url, 'rb', false, $context); $fp = @fopen($url, 'rb', false, $context);
if (!$fp) { if (!$fp) {
throw new Exception('Could not post to '. $url); throw new Exception(sprintf(t('Could not post to %s'), $url));
} }
$response = @stream_get_contents($fp); $response = @stream_get_contents($fp);
if ($response === false) { if ($response === false) {
throw new Exception('Bad response from the hub '. $url); throw new Exception(sprintf(t('Bad response from the hub %s'), $url));
} }
return $response; return $response;
} }
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function pubsubhubbub_dummy_translation()
{
// meta
t('Enable PubSubHubbub feed publishing.');
}

View file

@ -1 +1 @@
description="For each link, add a QRCode icon ." description="For each link, add a QRCode icon."

View file

@ -59,3 +59,12 @@ function hook_qrcode_render_includes($data)
return $data; return $data;
} }
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function qrcode_dummy_translation()
{
// meta
t('For each link, add a QRCode icon.');
}

View file

@ -1 +1,5 @@
<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> <span>
<a href="%s%s" target="_blank">
<img class="linklist-plugin-icon" src="%s/wallabag/wallabag.png" title="%s" alt="wallabag" />
</a>
</span>

View file

@ -5,6 +5,7 @@
*/ */
require_once 'WallabagInstance.php'; require_once 'WallabagInstance.php';
use Shaarli\Config\ConfigManager;
/** /**
* Init function, return an error if the server is not set. * Init function, return an error if the server is not set.
@ -17,8 +18,8 @@ function wallabag_init($conf)
{ {
$wallabagUrl = $conf->get('plugins.WALLABAG_URL'); $wallabagUrl = $conf->get('plugins.WALLABAG_URL');
if (empty($wallabagUrl)) { if (empty($wallabagUrl)) {
$error = 'Wallabag plugin error: '. $error = t('Wallabag plugin error: '.
'Please define the "WALLABAG_URL" setting in the plugin administration page.'; 'Please define the "WALLABAG_URL" setting in the plugin administration page.');
return array($error); return array($error);
} }
} }
@ -43,12 +44,14 @@ function hook_wallabag_render_linklist($data, $conf)
$wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html'); $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html');
$linkTitle = t('Save to wallabag');
foreach ($data['links'] as &$value) { foreach ($data['links'] as &$value) {
$wallabag = sprintf( $wallabag = sprintf(
$wallabagHtml, $wallabagHtml,
$wallabagInstance->getWallabagUrl(), $wallabagInstance->getWallabagUrl(),
urlencode($value['url']), urlencode($value['url']),
PluginManager::$PLUGINS_PATH PluginManager::$PLUGINS_PATH,
$linkTitle
); );
$value['link_plugin'][] = $wallabag; $value['link_plugin'][] = $wallabag;
} }
@ -56,3 +59,14 @@ function hook_wallabag_render_linklist($data, $conf)
return $data; return $data;
} }
/**
* This function is never called, but contains translation calls for GNU gettext extraction.
*/
function wallabag_dummy_translation()
{
// meta
t('For each link, add a QRCode icon.');
t('Wallabag API URL');
t('Wallabag API version (1 or 2)');
}

View file

@ -1,41 +1,203 @@
<?php <?php
require_once 'application/Languages.php'; namespace Shaarli;
use Shaarli\Config\ConfigManager;
/** /**
* Class LanguagesTest. * Class LanguagesTest.
*/ */
class LanguagesTest extends PHPUnit_Framework_TestCase class LanguagesTest extends \PHPUnit_Framework_TestCase
{ {
/**
* @var string Config file path (without extension).
*/
protected static $configFile = 'tests/utils/config/configJson';
/**
* @var ConfigManager
*/
protected $conf;
/**
*
*/
public function setUp()
{
$this->conf = new ConfigManager(self::$configFile);
}
/** /**
* Test t() with a simple non identified value. * Test t() with a simple non identified value.
*/ */
public function testTranslateSingleNotID() public function testTranslateSingleNotIDGettext()
{ {
$this->conf->set('translation.mode', 'gettext');
new Languages('en', $this->conf);
$text = 'abcdé 564 fgK'; $text = 'abcdé 564 fgK';
$this->assertEquals($text, t($text)); $this->assertEquals($text, t($text));
} }
/** /**
* Test t() with a non identified plural form. * Test t() with a simple identified value in gettext mode.
*/ */
public function testTranslatePluralNotID() public function testTranslateSingleIDGettext()
{ {
$text = '%s sandwich'; $this->conf->set('translation.mode', 'gettext');
$nText = '%s sandwiches'; new Languages('en', $this->conf);
$this->assertEquals('0 sandwich', t($text, $nText)); $text = 'permalink';
$this->assertEquals('1 sandwich', t($text, $nText, 1)); $this->assertEquals($text, t($text));
$this->assertEquals('2 sandwiches', t($text, $nText, 2));
} }
/** /**
* Test t() with a non identified invalid plural form. * Test t() with a non identified plural form in gettext mode.
*/ */
public function testTranslatePluralNotIDInvalid() public function testTranslatePluralNotIDGettext()
{ {
$this->conf->set('translation.mode', 'gettext');
new Languages('en', $this->conf);
$text = 'sandwich'; $text = 'sandwich';
$nText = 'sandwiches'; $nText = 'sandwiches';
$this->assertEquals('sandwiches', t($text, $nText, 0));
$this->assertEquals('sandwich', t($text, $nText, 1)); $this->assertEquals('sandwich', t($text, $nText, 1));
$this->assertEquals('sandwiches', t($text, $nText, 2)); $this->assertEquals('sandwiches', t($text, $nText, 2));
} }
/**
* Test t() with an identified plural form in gettext mode.
*/
public function testTranslatePluralIDGettext()
{
$this->conf->set('translation.mode', 'gettext');
new Languages('en', $this->conf);
$text = 'shaare';
$nText = 'shaares';
// In english, zero is followed by plural form
$this->assertEquals('shaares', t($text, $nText, 0));
$this->assertEquals('shaare', t($text, $nText, 1));
$this->assertEquals('shaares', t($text, $nText, 2));
}
/**
* Test t() with a simple non identified value.
*/
public function testTranslateSingleNotIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'abcdé 564 fgK';
$this->assertEquals($text, t($text));
}
/**
* Test t() with a simple identified value in PHP mode.
*/
public function testTranslateSingleIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'permalink';
$this->assertEquals($text, t($text));
}
/**
* Test t() with a non identified plural form in PHP mode.
*/
public function testTranslatePluralNotIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'sandwich';
$nText = 'sandwiches';
$this->assertEquals('sandwiches', t($text, $nText, 0));
$this->assertEquals('sandwich', t($text, $nText, 1));
$this->assertEquals('sandwiches', t($text, $nText, 2));
}
/**
* Test t() with an identified plural form in PHP mode.
*/
public function testTranslatePluralIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'shaare';
$nText = 'shaares';
// In english, zero is followed by plural form
$this->assertEquals('shaares', t($text, $nText, 0));
$this->assertEquals('shaare', t($text, $nText, 1));
$this->assertEquals('shaares', t($text, $nText, 2));
}
/**
* Test t() with an invalid language set in the configuration in gettext mode.
*/
public function testTranslateWithInvalidConfLanguageGettext()
{
$this->conf->set('translation.mode', 'gettext');
$this->conf->set('translation.language', 'nope');
new Languages('fr', $this->conf);
$text = 'grumble';
$this->assertEquals($text, t($text));
}
/**
* Test t() with an invalid language set in the configuration in PHP mode.
*/
public function testTranslateWithInvalidConfLanguagePhp()
{
$this->conf->set('translation.mode', 'php');
$this->conf->set('translation.language', 'nope');
new Languages('fr', $this->conf);
$text = 'grumble';
$this->assertEquals($text, t($text));
}
/**
* Test t() with an invalid language set with auto language in gettext mode.
*/
public function testTranslateWithInvalidAutoLanguageGettext()
{
$this->conf->set('translation.mode', 'gettext');
new Languages('nope', $this->conf);
$text = 'grumble';
$this->assertEquals($text, t($text));
}
/**
* Test t() with an invalid language set with auto language in PHP mode.
*/
public function testTranslateWithInvalidAutoLanguagePhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('nope', $this->conf);
$text = 'grumble';
$this->assertEquals($text, t($text));
}
/**
* Test t() with an extension language file in gettext mode
*/
public function testTranslationExtensionGettext()
{
$this->conf->set('translation.mode', 'gettext');
$this->conf->set('translation.extensions.test', 'tests/utils/languages/');
new Languages('en', $this->conf);
$txt = 'car'; // ignore me poedit
$this->assertEquals('car', t($txt, $txt, 1, 'test'));
$this->assertEquals('Search', t('Search', 'Search', 1, 'test'));
}
/**
* Test t() with an extension language file in PHP mode
*/
public function testTranslationExtensionPhp()
{
$this->conf->set('translation.mode', 'php');
$this->conf->set('translation.extensions.test', 'tests/utils/languages/');
new Languages('en', $this->conf);
$txt = 'car'; // ignore me poedit
$this->assertEquals('car', t($txt, $txt, 1, 'test'));
$this->assertEquals('Search', t('Search', 'Search', 1, 'test'));
}
} }

View file

@ -384,18 +384,18 @@ public function testReturnBytes()
*/ */
public function testHumanBytes() public function testHumanBytes()
{ {
$this->assertEquals('2kiB', human_bytes(2 * 1024)); $this->assertEquals('2'. t('kiB'), human_bytes(2 * 1024));
$this->assertEquals('2kiB', human_bytes(strval(2 * 1024))); $this->assertEquals('2'. t('kiB'), human_bytes(strval(2 * 1024)));
$this->assertEquals('2MiB', human_bytes(2 * (pow(1024, 2)))); $this->assertEquals('2'. t('MiB'), human_bytes(2 * (pow(1024, 2))));
$this->assertEquals('2MiB', human_bytes(strval(2 * (pow(1024, 2))))); $this->assertEquals('2'. t('MiB'), human_bytes(strval(2 * (pow(1024, 2)))));
$this->assertEquals('2GiB', human_bytes(2 * (pow(1024, 3)))); $this->assertEquals('2'. t('GiB'), human_bytes(2 * (pow(1024, 3))));
$this->assertEquals('2GiB', human_bytes(strval(2 * (pow(1024, 3))))); $this->assertEquals('2'. t('GiB'), human_bytes(strval(2 * (pow(1024, 3)))));
$this->assertEquals('374B', human_bytes(374)); $this->assertEquals('374'. t('B'), human_bytes(374));
$this->assertEquals('374B', human_bytes('374')); $this->assertEquals('374'. t('B'), human_bytes('374'));
$this->assertEquals('232kiB', human_bytes(237481)); $this->assertEquals('232'. t('kiB'), human_bytes(237481));
$this->assertEquals('Unlimited', human_bytes('0')); $this->assertEquals(t('Unlimited'), human_bytes('0'));
$this->assertEquals('Unlimited', human_bytes(0)); $this->assertEquals(t('Unlimited'), human_bytes(0));
$this->assertEquals('Setting not set', human_bytes('')); $this->assertEquals(t('Setting not set'), human_bytes(''));
} }
/** /**
@ -403,9 +403,9 @@ public function testHumanBytes()
*/ */
public function testGetMaxUploadSize() public function testGetMaxUploadSize()
{ {
$this->assertEquals('1MiB', get_max_upload_size(2097152, '1024k')); $this->assertEquals('1'. t('MiB'), get_max_upload_size(2097152, '1024k'));
$this->assertEquals('1MiB', get_max_upload_size('1m', '2m')); $this->assertEquals('1'. t('MiB'), get_max_upload_size('1m', '2m'));
$this->assertEquals('100B', get_max_upload_size(100, 100)); $this->assertEquals('100'. t('B'), get_max_upload_size(100, 100));
} }
/** /**

6
tests/bootstrap.php Normal file
View file

@ -0,0 +1,6 @@
<?php
require_once 'vendor/autoload.php';
$conf = new \Shaarli\Config\ConfigManager('tests/utils/config/configJson');
new \Shaarli\Languages('en', $conf);

View file

@ -1,7 +1,6 @@
<?php <?php
if (! empty('UT_LOCALE')) { require_once 'tests/bootstrap.php';
if (! empty(getenv('UT_LOCALE'))) {
setlocale(LC_ALL, getenv('UT_LOCALE')); setlocale(LC_ALL, getenv('UT_LOCALE'));
} }
require_once 'vendor/autoload.php';

View file

@ -0,0 +1,175 @@
<?php
namespace Shaarli;
use Shaarli\Config\ConfigManager;
/**
* Class LanguagesFrTest
*
* Test the translation system in PHP and gettext mode with French language.
*
* @package Shaarli
*/
class LanguagesFrTest extends \PHPUnit_Framework_TestCase
{
/**
* @var string Config file path (without extension).
*/
protected static $configFile = 'tests/utils/config/configJson';
/**
* @var ConfigManager
*/
protected $conf;
/**
* Init: force French
*/
public function setUp()
{
$this->conf = new ConfigManager(self::$configFile);
$this->conf->set('translation.language', 'fr');
}
/**
* Reset the locale since gettext seems to mess with it, making it too long
*/
public static function tearDownAfterClass()
{
if (! empty(getenv('UT_LOCALE'))) {
setlocale(LC_ALL, getenv('UT_LOCALE'));
}
}
/**
* Test t() with a simple non identified value.
*/
public function testTranslateSingleNotIDGettext()
{
$this->conf->set('translation.mode', 'gettext');
new Languages('en', $this->conf);
$text = 'abcdé 564 fgK';
$this->assertEquals($text, t($text));
}
/**
* Test t() with a simple identified value in gettext mode.
*/
public function testTranslateSingleIDGettext()
{
$this->conf->set('translation.mode', 'gettext');
new Languages('en', $this->conf);
$text = 'permalink';
$this->assertEquals('permalien', t($text));
}
/**
* Test t() with a non identified plural form in gettext mode.
*/
public function testTranslatePluralNotIDGettext()
{
$this->conf->set('translation.mode', 'gettext');
new Languages('en', $this->conf);
$text = 'sandwich';
$nText = 'sandwiches';
// Not ID, so English fallback, and in english, plural 0
$this->assertEquals('sandwiches', t($text, $nText, 0));
$this->assertEquals('sandwich', t($text, $nText, 1));
$this->assertEquals('sandwiches', t($text, $nText, 2));
}
/**
* Test t() with an identified plural form in gettext mode.
*/
public function testTranslatePluralIDGettext()
{
$this->conf->set('translation.mode', 'gettext');
new Languages('en', $this->conf);
$text = 'shaare';
$nText = 'shaares';
$this->assertEquals('shaare', t($text, $nText, 0));
$this->assertEquals('shaare', t($text, $nText, 1));
$this->assertEquals('shaares', t($text, $nText, 2));
}
/**
* Test t() with a simple non identified value.
*/
public function testTranslateSingleNotIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'abcdé 564 fgK';
$this->assertEquals($text, t($text));
}
/**
* Test t() with a simple identified value in PHP mode.
*/
public function testTranslateSingleIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'permalink';
$this->assertEquals('permalien', t($text));
}
/**
* Test t() with a non identified plural form in PHP mode.
*/
public function testTranslatePluralNotIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'sandwich';
$nText = 'sandwiches';
// Not ID, so English fallback, and in english, plural 0
$this->assertEquals('sandwiches', t($text, $nText, 0));
$this->assertEquals('sandwich', t($text, $nText, 1));
$this->assertEquals('sandwiches', t($text, $nText, 2));
}
/**
* Test t() with an identified plural form in PHP mode.
*/
public function testTranslatePluralIDPhp()
{
$this->conf->set('translation.mode', 'php');
new Languages('en', $this->conf);
$text = 'shaare';
$nText = 'shaares';
// In english, zero is followed by plural form
$this->assertEquals('shaare', t($text, $nText, 0));
$this->assertEquals('shaare', t($text, $nText, 1));
$this->assertEquals('shaares', t($text, $nText, 2));
}
/**
* Test t() with an extension language file in gettext mode
*/
public function testTranslationExtensionGettext()
{
$this->conf->set('translation.mode', 'gettext');
$this->conf->set('translation.extensions.test', 'tests/utils/languages/');
new Languages('en', $this->conf);
$txt = 'car'; // ignore me poedit
$this->assertEquals('voiture', t($txt, $txt, 1, 'test'));
$this->assertEquals('Fouille', t('Search', 'Search', 1, 'test'));
}
/**
* Test t() with an extension language file in PHP mode
*/
public function testTranslationExtensionPhp()
{
$this->conf->set('translation.mode', 'php');
$this->conf->set('translation.extensions.test', 'tests/utils/languages/');
new Languages('en', $this->conf);
$txt = 'car'; // ignore me poedit
$this->assertEquals('voiture', t($txt, $txt, 1, 'test'));
$this->assertEquals('Fouille', t('Search', 'Search', 1, 'test'));
}
}

Binary file not shown.

View file

@ -0,0 +1,19 @@
msgid ""
msgstr ""
"Project-Id-Version: Extension test\n"
"POT-Creation-Date: 2017-05-20 13:54+0200\n"
"PO-Revision-Date: 2017-05-20 14:16+0200\n"
"Last-Translator: \n"
"Language-Team: Shaarli\n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Poedit 2.0.1\n"
msgid "car"
msgstr "voiture"
msgid "Search"
msgstr "Fouille"

View file

@ -32,7 +32,7 @@ <h2 class="window-title">{"Manage tags"|t}</h2>
</div> </div>
</form> </form>
<p>You can also edit tags in the <a href="?do=taglist&sort=usage">tag list</a>.</p> <p>{'You can also edit tags in the'|t} <a href="?do=taglist&sort=usage">{'tag list'|t}</a>.</p>
</div> </div>
</div> </div>
{include="page.footer"} {include="page.footer"}

View file

@ -69,6 +69,30 @@ <h2 class="window-title">{'Configure'|t}</h2>
</div> </div>
</div> </div>
</div> </div>
<div class="pure-g">
<div class="pure-u-lg-{$ratioLabel} pure-u-1">
<div class="form-label">
<label for="language">
<span class="label-name">{'Language'|t}</span>
</label>
</div>
</div>
<div class="pure-u-lg-{$ratioInput} pure-u-1">
<div class="form-input">
<select name="language" id="language" class="align">
{loop="$languages"}
<option value="{$key}"
{if="$key===$language"}
selected="selected"
{/if}
>
{$value}
</option>
{/loop}
</select>
</div>
</div>
</div>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-lg-{$ratioLabel} pure-u-1 "> <div class="pure-u-lg-{$ratioLabel} pure-u-1 ">
<div class="form-label"> <div class="form-label">

View file

@ -18,7 +18,7 @@ <h2 class="window-title">{"Import Database"|t}</h2>
<div class="center" id="import-field"> <div class="center" id="import-field">
<input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}"> <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
<input type="file" name="filetoupload"> <input type="file" name="filetoupload">
<p><br>Maximum size allowed: <strong>{$maxfilesizeHuman}</strong></p> <p><br>{'Maximum size allowed:'|t} <strong>{$maxfilesizeHuman}</strong></p>
</div> </div>
<div class="pure-g"> <div class="pure-g">
@ -31,15 +31,15 @@ <h2 class="window-title">{"Import Database"|t}</h2>
<div class="radio-buttons"> <div class="radio-buttons">
<div> <div>
<input type="radio" name="privacy" value="default" checked="checked"> <input type="radio" name="privacy" value="default" checked="checked">
Use values from the imported file, default to public {'Use values from the imported file, default to public'|t}
</div> </div>
<div> <div>
<input type="radio" name="privacy" value="private"> <input type="radio" name="privacy" value="private">
Import all bookmarks as private {'Import all bookmarks as private'|t}
</div> </div>
<div> <div>
<input type="radio" name="privacy" value="public"> <input type="radio" name="privacy" value="public">
Import all bookmarks as public {'Import all bookmarks as public'|t}
</div> </div>
</div> </div>
</div> </div>

View file

@ -65,6 +65,27 @@ <h2 class="window-title">{'Install Shaarli'|t}</h2>
</div> </div>
</div> </div>
<div class="pure-g">
<div class="pure-u-lg-{$ratioLabel} pure-u-1">
<div class="form-label">
<label for="language">
<span class="label-name">{'Language'|t}</span>
</label>
</div>
</div>
<div class="pure-u-lg-{$ratioInput} pure-u-1">
<div class="form-input">
<select name="language" id="language" class="align">
{loop="$languages"}
<option value="{$key}">
{$value}
</option>
{/loop}
</select>
</div>
</div>
</div>
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-lg-{$ratioLabel} pure-u-1"> <div class="pure-u-lg-{$ratioLabel} pure-u-1">
<div class="form-label"> <div class="form-label">

View file

@ -138,6 +138,9 @@ window.onload = function () {
}); });
foldAllButton.firstElementChild.classList.toggle('fa-chevron-down'); foldAllButton.firstElementChild.classList.toggle('fa-chevron-down');
foldAllButton.firstElementChild.classList.toggle('fa-chevron-up'); foldAllButton.firstElementChild.classList.toggle('fa-chevron-up');
foldAllButton.title = state === 'down'
? document.getElementById('translation-fold-all').innerHTML
: document.getElementById('translation-expand-all').innerHTML
}); });
}); });
} }
@ -146,7 +149,7 @@ window.onload = function () {
{ {
// Switch fold/expand - up = fold // Switch fold/expand - up = fold
if (button.classList.contains('fa-chevron-up')) { if (button.classList.contains('fa-chevron-up')) {
button.title = 'Expand'; button.title = document.getElementById('translation-expand').innerHTML;
if (description != null) { if (description != null) {
description.style.display = 'none'; description.style.display = 'none';
} }
@ -155,7 +158,7 @@ window.onload = function () {
} }
} }
else { else {
button.title = 'Fold'; button.title = document.getElementById('translation-fold').innerHTML;
if (description != null) { if (description != null) {
description.style.display = 'block'; description.style.display = 'block';
} }
@ -173,7 +176,7 @@ window.onload = function () {
var deleteLinks = document.querySelectorAll('.confirm-delete'); var deleteLinks = document.querySelectorAll('.confirm-delete');
[].forEach.call(deleteLinks, function(deleteLink) { [].forEach.call(deleteLinks, function(deleteLink) {
deleteLink.addEventListener('click', function(event) { deleteLink.addEventListener('click', function(event) {
if(! confirm('Are you sure you want to delete this link ?')) { if(! confirm(document.getElementById('translation-delete-link').innerHTML)) {
event.preventDefault(); event.preventDefault();
} }
}); });
@ -618,7 +621,7 @@ function activateFirefoxSocial(node) {
// Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable. // Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable.
var data = { var data = {
name: title, name: title,
description: "The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community.", description: document.getElementById('translation-delete-link').innerHTML,
author: "Shaarli", author: "Shaarli",
version: "1.0.0", version: "1.0.0",

View file

@ -86,7 +86,7 @@
<div class="pure-g pure-alert pure-alert-success search-result"> <div class="pure-g pure-alert pure-alert-success search-result">
<div class="pure-u-2-24"></div> <div class="pure-u-2-24"></div>
<div class="pure-u-20-24"> <div class="pure-u-20-24">
{function="t('%s result', '%s results', $result_count)"} {function="sprintf(t('%s result', '%s results', $result_count), $result_count)"}
{if="!empty($search_term)"} {if="!empty($search_term)"}
{'for'|t} <em><strong>{$search_term}</strong></em> {'for'|t} <em><strong>{$search_term}</strong></em>
{/if} {/if}
@ -117,6 +117,16 @@
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-lg-2-24 pure-u-1-24"></div> <div class="pure-u-lg-2-24 pure-u-1-24"></div>
<div class="pure-u-lg-20-24 pure-u-22-24"> <div class="pure-u-lg-20-24 pure-u-22-24">
{ignore}Set translation here, for performances{/ignore}
{$strPrivate=t('Private')}
{$strEdit=t('Edit')}
{$strDelete=t('Delete')}
{$strFold=t('Fold')}
{$strEdited=t('Edited: ')}
{$strPermalink=t('Permalink')}
{$strPermalinkLc=t('permalink')}
{$strAddTag=t('Add tag')}
{ignore}End of translations{/ignore}
{loop="links"} {loop="links"}
<div class="anchor" id="{$value.shorturl}"></div> <div class="anchor" id="{$value.shorturl}"></div>
<div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}"> <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}">
@ -125,12 +135,12 @@
{if="isLoggedIn()"} {if="isLoggedIn()"}
<div class="linklist-item-editbuttons"> <div class="linklist-item-editbuttons">
{if="$value.private"} {if="$value.private"}
<span class="label label-private">{'Private'|t}</span> <span class="label label-private">{$strPrivate}</span>
{/if} {/if}
<input type="checkbox" class="delete-checkbox" value="{$value.id}"> <input type="checkbox" class="delete-checkbox" value="{$value.id}">
<!-- FIXME! JS translation --> <!-- FIXME! JS translation -->
<a href="?edit_link={$value.id}" title="{'Edit'|t}"><i class="fa fa-pencil-square-o edit-link"></i></a> <a href="?edit_link={$value.id}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link"></i></a>
<a href="#" title="{'Fold'|t}" class="fold-button"><i class="fa fa-chevron-up"></i></a> <a href="#" title="{$strFold}" class="fold-button"><i class="fa fa-chevron-up"></i></a>
</div> </div>
{/if} {/if}
@ -164,7 +174,7 @@ <h2>
<i class="fa fa-tags"></i> <i class="fa fa-tags"></i>
{$tag_counter=count($value.taglist)} {$tag_counter=count($value.taglist)}
{loop="value.taglist"} {loop="value.taglist"}
<span class="label label-tag" title="Add tag"> <span class="label label-tag" title="{$strAddTag}">
<a href="?addtag={$value|urlencode}">{$value}</a> <a href="?addtag={$value|urlencode}">{$value}</a>
</span> </span>
{if="$tag_counter - 1 != $counter"}&middot;{/if} {if="$tag_counter - 1 != $counter"}&middot;{/if}
@ -174,9 +184,9 @@ <h2>
<div class="pure-g"> <div class="pure-g">
<div class="linklist-item-infos-dateblock pure-u-lg-3-8 pure-u-1"> <div class="linklist-item-infos-dateblock pure-u-lg-3-8 pure-u-1">
<a href="?{$value.shorturl}" title="{'Permalink'|t}"> <a href="?{$value.shorturl}" title="{$strPermalink}">
{if="!$hide_timestamps || isLoggedIn()"} {if="!$hide_timestamps || isLoggedIn()"}
{$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'} {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink}
<span class="linkdate" title="{$updated}"> <span class="linkdate" title="{$updated}">
<i class="fa fa-clock-o"></i> <i class="fa fa-clock-o"></i>
{$value.created|format_date} {$value.created|format_date}
@ -184,7 +194,7 @@ <h2>
&middot; &middot;
</span> </span>
{/if} {/if}
{'permalink'|t} {$strPermalinkLc}
</a> </a>
<div class="pure-u-0 pure-u-lg-visible"> <div class="pure-u-0 pure-u-lg-visible">
@ -205,7 +215,7 @@ <h2>
</a> </a>
{if="isLoggedIn()"} {if="isLoggedIn()"}
<a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}" <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}"
title="{'Delete'|t}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete"> title="{$strDelete}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete">
<i class="fa fa-trash"></i> <i class="fa fa-trash"></i>
</a> </a>
{/if} {/if}
@ -221,7 +231,7 @@ <h2>
{if="isLoggedIn()"} {if="isLoggedIn()"}
&middot; &middot;
<a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}" <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}"
title="{'Delete'|t}" class="delete-link confirm-delete"> title="{$strDelete}" class="delete-link confirm-delete">
<i class="fa fa-trash"></i> <i class="fa fa-trash"></i>
</a> </a>
{/if} {/if}

View file

@ -13,7 +13,7 @@
<a href="?untaggedonly" title="{'Filter untagged links'|t}" <a href="?untaggedonly" title="{'Filter untagged links'|t}"
class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if} class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if}
><i class="fa fa-tag"></i></a> ><i class="fa fa-tag"></i></a>
<a href="#" class="filter-off fold-all pure-u-lg-0" title="Fold all"> <a href="#" class="filter-off fold-all pure-u-lg-0" title="{'Fold all'|t}">
<i class="fa fa-chevron-up"></i> <i class="fa fa-chevron-up"></i>
</a> </a>
{loop="$action_plugin"} {loop="$action_plugin"}
@ -53,7 +53,7 @@
<form method="GET" class="pure-u-0 pure-u-lg-visible"> <form method="GET" class="pure-u-0 pure-u-lg-visible">
<input type="text" name="linksperpage" placeholder="133"> <input type="text" name="linksperpage" placeholder="133">
</form> </form>
<a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" title="Fold all"> <a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" title="{'Fold all'|t}">
<i class="fa fa-chevron-up"></i> <i class="fa fa-chevron-up"></i>
</a> </a>
</div> </div>

View file

@ -8,8 +8,8 @@
{$version} {$version}
{/if} {/if}
&middot; &middot;
The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community &middot; {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} &middot;
<a href="doc/html/index.html" rel="nofollow">Documentation</a> <a href="doc/html/index.html" rel="nofollow">{'Documentation'|t}</a>
{loop="$plugins_footer.text"} {loop="$plugins_footer.text"}
{$value} {$value}
{/loop} {/loop}
@ -27,6 +27,17 @@
<script src="{$value}#"></script> <script src="{$value}#"></script>
{/loop} {/loop}
<div id="js-translations" class="hidden">
<span id="translation-fold">{'Fold'|t}</span>
<span id="translation-fold-all">{'Fold all'|t}</span>
<span id="translation-expand">{'Expand'|t}</span>
<span id="translation-expand-all">{'Expand all'|t}</span>
<span id="translation-delete-link">{'Are you sure you want to delete this link?'|t}</span>
<span id="translation-shaarli-desc">
{'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t}
</span>
</div>
<script src="js/shaarli.js?v={$version_hash}"></script> <script src="js/shaarli.js?v={$version_hash}"></script>
<script src="inc/awesomplete.js?v={$version_hash}#"></script> <script src="inc/awesomplete.js?v={$version_hash}#"></script>
<script src="inc/awesomplete-multiple-tags.js?v={$version_hash}#"></script> <script src="inc/awesomplete-multiple-tags.js?v={$version_hash}#"></script>

View file

@ -116,8 +116,8 @@ <h3 class="window-subtitle">{'Disabled Plugins'|t}</h3>
</section> </section>
<div class="center more"> <div class="center more">
More plugins available {"More plugins available"|t}
<a href="doc/Community-&-Related-software.html#third-party-plugins">in the documentation</a>. <a href="doc/Community-&-Related-software.html#third-party-plugins">{"in the documentation"|t}</a>.
</div> </div>
<div class="center"> <div class="center">
<input type="submit" value="{'Save'|t}" name="save"> <input type="submit" value="{'Save'|t}" name="save">