Add Internationalization

- Add Utils/L10n class
- Add translator functions to PHP Renderer
- Refactor web controllers to prevent duplicated code
- Add locale middleware
- Add translation file loading
- Add i18n settings
This commit is contained in:
Hypolite Petovan 2018-11-15 23:59:00 -05:00
parent 13a2068a8b
commit 5b7bb030de
21 changed files with 537 additions and 245 deletions

View file

@ -1,141 +0,0 @@
<?php
namespace Friendica\Directory\Content;
/**
* @author Hypolite Petovan <mrpetovan@gmail.com>
*/
class L10n
{
/**
* @var string
*/
private $lang;
/**
* @var array
*/
private $strings;
/**
* @var string
*/
private $lang_path;
public function __construct(string $language = 'en', string $lang_path = '')
{
$this->lang = $language;
$this->lang_path = $lang_path;
$this->loadTranslationTable();
}
/**
* Loads string translation table
*
* First addon strings are loaded, then globals
*
* Uses an App object shim since all the strings files refer to $a->strings
*
* @param string $lang language code to load
*/
private function loadTranslationTable(): void
{
if (file_exists($this->lang_path . '/' . $this->lang . '/strings.php')) {
$this->strings = include $this->lang_path . '/' . $this->lang . '/strings.php';
}
}
/**
* @brief Return the localized version of a singular/plural string with optional string interpolation
*
* This function takes two english strings as parameters, singular and plural, as
* well as a count. If a localized version exists for the current language, they
* are used instead. Discrimination between singular and plural is done using the
* localized function if any or the default one. Finally, a string interpolation
* is performed using the count as parameter.
*
* Usages:
* - L10n::tt('Like', 'Likes', $count)
* - L10n::tt("%s user deleted", "%s users deleted", count($users))
*
* @param string $singular
* @param string $plural
* @param int $count
* @return string
*/
public function tt(string $singular, string $plural, int $count): string
{
if (!empty($this->strings[$singular])) {
$t = $this->strings[$singular];
if (is_array($t)) {
$plural_function = 'string_plural_select_' . str_replace('-', '_', $this->lang);
if (function_exists($plural_function)) {
$i = $plural_function($count);
} else {
$i = $this->stringPluralSelectDefault($count);
}
// for some languages there is only a single array item
if (!isset($t[$i])) {
$s = $t[0];
} else {
$s = $t[$i];
}
} else {
$s = $t;
}
} elseif ($this->stringPluralSelectDefault($count)) {
$s = $plural;
} else {
$s = $singular;
}
$s = @sprintf($s, $count);
return $s;
}
/**
* @brief Return the localized version of the provided string with optional string interpolation
*
* This function takes a english string as parameter, and if a localized version
* exists for the current language, substitutes it before performing an eventual
* string interpolation (sprintf) with additional optional arguments.
*
* Usages:
* - L10n::t('This is an example')
* - L10n::t('URL %s returned no result', $url)
* - L10n::t('Current version: %s, new version: %s', $current_version, $new_version)
*
* @param string $s
* @param array $vars Variables to interpolate in the translation string
* @return string
*/
public function t($s, ...$vars): string
{
if (empty($s)) {
return '';
}
if (!empty($this->strings[$s])) {
$t = $this->strings[$s];
$s = is_array($t) ? $t[0] : $t;
}
if (count($vars) > 0) {
$s = sprintf($s, ...$vars);
}
return $s;
}
/**
* Provide a fallback which will not collide with a function defined in any language file
*/
private function stringPluralSelectDefault(int $n): bool
{
return $n != 1;
}
}

View file

@ -24,7 +24,7 @@ class Pager
private $baseQueryString = ''; private $baseQueryString = '';
/** /**
* @var \Friendica\Directory\Content\L10n * @var \Gettext\TranslatorInterface
*/ */
private $l10n; private $l10n;
@ -33,11 +33,11 @@ class Pager
* *
* Guesses the page number from the GET parameter 'page'. * Guesses the page number from the GET parameter 'page'.
* *
* @param \Friendica\Directory\Content\L10n $l10n * @param \Gettext\TranslatorInterface $l10n
* @param \Psr\Http\Message\ServerRequestInterface $request * @param \Psr\Http\Message\ServerRequestInterface $request
* @param integer $itemsPerPage An optional number of items per page to override the default value * @param integer $itemsPerPage An optional number of items per page to override the default value
*/ */
public function __construct(L10n $l10n, \Psr\Http\Message\ServerRequestInterface $request, int $itemsPerPage = 50) public function __construct(\Gettext\TranslatorInterface $l10n, \Psr\Http\Message\ServerRequestInterface $request, int $itemsPerPage = 50)
{ {
$this->l10n = $l10n; $this->l10n = $l10n;
$this->setQueryString($request); $this->setQueryString($request);
@ -157,7 +157,7 @@ class Pager
* @param integer $itemCount The number of displayed items on the page * @param integer $itemCount The number of displayed items on the page
* @return array of links * @return array of links
*/ */
public function renderMinimal(int $itemCount, string $previous_label = 'Previous', string $next_label = 'Next') public function renderMinimal(int $itemCount)
{ {
$displayedItemCount = max(0, $itemCount); $displayedItemCount = max(0, $itemCount);
@ -165,12 +165,12 @@ class Pager
'class' => 'pager', 'class' => 'pager',
'prev' => [ 'prev' => [
'url' => $this->ensureQueryParameter($this->baseQueryString . '&page=' . ($this->getPage() - 1)), 'url' => $this->ensureQueryParameter($this->baseQueryString . '&page=' . ($this->getPage() - 1)),
'text' => $this->l10n->t($previous_label), 'text' => $this->l10n->gettext('Previous'),
'class' => 'previous' . ($this->getPage() == 1 ? ' disabled' : '') 'class' => 'previous' . ($this->getPage() == 1 ? ' disabled' : '')
], ],
'next' => [ 'next' => [
'url' => $this->ensureQueryParameter($this->baseQueryString . '&page=' . ($this->getPage() + 1)), 'url' => $this->ensureQueryParameter($this->baseQueryString . '&page=' . ($this->getPage() + 1)),
'text' => $this->l10n->t($next_label), 'text' => $this->l10n->gettext('Next'),
'class' => 'next' . ($displayedItemCount <= 0 ? ' disabled' : '') 'class' => 'next' . ($displayedItemCount <= 0 ? ' disabled' : '')
] ]
]; ];
@ -208,12 +208,12 @@ class Pager
if ($totalItemCount > $this->getItemsPerPage()) { if ($totalItemCount > $this->getItemsPerPage()) {
$data['first'] = [ $data['first'] = [
'url' => $this->ensureQueryParameter($this->baseQueryString . '&page=1'), 'url' => $this->ensureQueryParameter($this->baseQueryString . '&page=1'),
'text' => $this->l10n->t('First'), 'text' => $this->l10n->gettext('First'),
'class' => $this->getPage() == 1 ? 'disabled' : '' 'class' => $this->getPage() == 1 ? 'disabled' : ''
]; ];
$data['prev'] = [ $data['prev'] = [
'url' => $this->ensureQueryParameter($this->baseQueryString . '&page=' . ($this->getPage() - 1)), 'url' => $this->ensureQueryParameter($this->baseQueryString . '&page=' . ($this->getPage() - 1)),
'text' => $this->l10n->t('Previous'), 'text' => $this->l10n->gettext('Previous'),
'class' => $this->getPage() == 1 ? 'disabled' : '' 'class' => $this->getPage() == 1 ? 'disabled' : ''
]; ];
@ -270,12 +270,12 @@ class Pager
$data['next'] = [ $data['next'] = [
'url' => $this->ensureQueryParameter($this->baseQueryString . '&page=' . ($this->getPage() + 1)), 'url' => $this->ensureQueryParameter($this->baseQueryString . '&page=' . ($this->getPage() + 1)),
'text' => $this->l10n->t('Next'), 'text' => $this->l10n->gettext('Next'),
'class' => $this->getPage() == $lastpage ? 'disabled' : '' 'class' => $this->getPage() == $lastpage ? 'disabled' : ''
]; ];
$data['last'] = [ $data['last'] = [
'url' => $this->ensureQueryParameter($this->baseQueryString . '&page=' . $lastpage), 'url' => $this->ensureQueryParameter($this->baseQueryString . '&page=' . $lastpage),
'text' => $this->l10n->t('Last'), 'text' => $this->l10n->gettext('Last'),
'class' => $this->getPage() == $lastpage ? 'disabled' : '' 'class' => $this->getPage() == $lastpage ? 'disabled' : ''
]; ];
} }

View file

@ -21,14 +21,14 @@ class Search
*/ */
private $profileModel; private $profileModel;
/** /**
* @var \Friendica\Directory\Content\L10n * @var \Gettext\TranslatorInterface
*/ */
private $l10n; private $l10n;
public function __construct( public function __construct(
\Atlas\Pdo\Connection $atlas, \Atlas\Pdo\Connection $atlas,
\Friendica\Directory\Models\Profile $profileModel, \Friendica\Directory\Models\Profile $profileModel,
\Friendica\Directory\Content\L10n $l10n \Gettext\TranslatorInterface $l10n
) )
{ {
$this->atlas = $atlas; $this->atlas = $atlas;
@ -66,11 +66,12 @@ AND `account_type` = :account_type';
$count = $this->profileModel->getCountForDisplay($sql_where, $values); $count = $this->profileModel->getCountForDisplay($sql_where, $values);
$vars = [ $vars = [
'query' => $originalQuery, 'query' => $originalQuery,
'page' => $pager->getPage(), 'field' => $field,
'page' => $pager->getPage(),
'itemsperpage' => $pager->getItemsPerPage(), 'itemsperpage' => $pager->getItemsPerPage(),
'count' => $count, 'count' => $count,
'profiles' => $profiles 'profiles' => $profiles
]; ];
// Render index view // Render index view

View file

@ -0,0 +1,9 @@
<?php
namespace Friendica\Directory\Controllers\Web;
abstract class BaseController
{
}

View file

@ -2,17 +2,17 @@
namespace Friendica\Directory\Controllers\Web; namespace Friendica\Directory\Controllers\Web;
use \Friendica\Directory\Content\Pager; use Friendica\Directory\Content\Pager;
use \Friendica\Directory\Views\Widget\PopularCountries; use Friendica\Directory\Views\Widget\PopularCountries;
use \Friendica\Directory\Views\Widget\PopularTags; use Friendica\Directory\Views\Widget\PopularLanguages;
use PDO; use Friendica\Directory\Views\Widget\PopularTags;
use Slim\Http\Request; use Slim\Http\Request;
use Slim\Http\Response; use Slim\Http\Response;
/** /**
* @author Hypolite Petovan <mrpetovan@gmail.com> * @author Hypolite Petovan <mrpetovan@gmail.com>
*/ */
class Directory class Directory extends BaseController
{ {
/** /**
* @var \Atlas\Pdo\Connection * @var \Atlas\Pdo\Connection
@ -31,7 +31,7 @@ class Directory
*/ */
private $renderer; private $renderer;
/** /**
* @var \Friendica\Directory\Content\L10n * @var \Gettext\TranslatorInterface
*/ */
private $l10n; private $l10n;
@ -40,7 +40,7 @@ class Directory
\Friendica\Directory\Models\Profile $profileModel, \Friendica\Directory\Models\Profile $profileModel,
\Friendica\Directory\Views\Widget\AccountTypeTabs $accountTypeTabs, \Friendica\Directory\Views\Widget\AccountTypeTabs $accountTypeTabs,
\Friendica\Directory\Views\PhpRenderer $renderer, \Friendica\Directory\Views\PhpRenderer $renderer,
\Friendica\Directory\Content\L10n $l10n \Gettext\TranslatorInterface $l10n
) )
{ {
$this->atlas = $atlas; $this->atlas = $atlas;
@ -50,10 +50,11 @@ class Directory
$this->l10n = $l10n; $this->l10n = $l10n;
} }
public function render(Request $request, Response $response, array $args): Response public function render(Request $request, Response $response, array $args): array
{ {
$popularTags = new PopularTags($this->atlas, $this->renderer); $popularTags = new PopularTags($this->atlas, $this->renderer);
$popularCountries = new PopularCountries($this->atlas, $this->renderer); $popularCountries = new PopularCountries($this->atlas, $this->renderer);
$popularLanguages = new PopularLanguages($this->atlas, $this->renderer);
$pager = new Pager($this->l10n, $request, 20); $pager = new Pager($this->l10n, $request, 20);
@ -69,18 +70,19 @@ class Directory
$count = $this->profileModel->getCountForDisplay($condition, $values); $count = $this->profileModel->getCountForDisplay($condition, $values);
$vars = [ $vars = [
'title' => $this->l10n->t('People'), 'title' => $this->l10n->gettext('People'),
'profiles' => $profiles, 'profiles' => $profiles,
'pager_full' => $pager->renderFull($count), 'pager_full' => $pager->renderFull($count),
'pager_minimal' => $pager->renderMinimal($count), 'pager_minimal' => $pager->renderMinimal($count),
'accountTypeTabs' => $this->accountTypeTabs->render('directory', $args['account_type'] ?? ''), 'accountTypeTabs' => $this->accountTypeTabs->render('directory', $args['account_type'] ?? ''),
'popularTags' => $popularTags->render(), 'popularTags' => $popularTags->render(),
'popularCountries' => $popularCountries->render(), 'popularCountries' => $popularCountries->render(),
'popularLanguages' => $popularLanguages->render(),
]; ];
$content = $this->renderer->fetch('directory.phtml', $vars); $content = $this->renderer->fetch('directory.phtml', $vars);
// Render index view // Render index view
return $this->renderer->render($response, 'layout.phtml', ['baseUrl' => $request->getUri()->getBaseUrl(), 'content' => $content]); return ['content' => $content];
} }
} }

View file

@ -10,7 +10,7 @@ use Psr\Http\Message\ServerRequestInterface;
/** /**
* @author Hypolite Petovan <mrpetovan@gmail.com> * @author Hypolite Petovan <mrpetovan@gmail.com>
*/ */
class Search class Search extends BaseController
{ {
/** /**
* @var \Atlas\Pdo\Connection * @var \Atlas\Pdo\Connection
@ -29,7 +29,7 @@ class Search
*/ */
private $accountTypeTabs; private $accountTypeTabs;
/** /**
* @var \Friendica\Directory\Content\L10n * @var \Gettext\TranslatorInterface
*/ */
private $l10n; private $l10n;
@ -38,7 +38,7 @@ class Search
\Friendica\Directory\Models\Profile $profileModel, \Friendica\Directory\Models\Profile $profileModel,
\Friendica\Directory\Views\Widget\AccountTypeTabs $accountTypeTabs, \Friendica\Directory\Views\Widget\AccountTypeTabs $accountTypeTabs,
\Friendica\Directory\Views\PhpRenderer $renderer, \Friendica\Directory\Views\PhpRenderer $renderer,
\Friendica\Directory\Content\L10n $l10n \Gettext\TranslatorInterface $l10n
) )
{ {
$this->atlas = $atlas; $this->atlas = $atlas;
@ -48,7 +48,7 @@ class Search
$this->l10n = $l10n; $this->l10n = $l10n;
} }
public function render(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args): \Slim\Http\Response public function render(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args): array
{ {
$pager = new Pager($this->l10n, $request, 20); $pager = new Pager($this->l10n, $request, 20);
@ -56,9 +56,20 @@ class Search
$field = filter_input(INPUT_GET, 'field', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW & FILTER_FLAG_STRIP_HIGH); $field = filter_input(INPUT_GET, 'field', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW & FILTER_FLAG_STRIP_HIGH);
$fieldName = '';
if ($field) { if ($field) {
$query .= '%'; $query .= '%';
$sql_where = '`' . $field . '` LIKE :query'; $sql_where = 'p.`' . $field . '` LIKE :query';
switch($field) {
case 'language': $fieldName = $this->l10n->pgettext('field', 'Language'); break;
case 'locality': $fieldName = $this->l10n->pgettext('field', 'Locality'); break;
case 'region' : $fieldName = $this->l10n->pgettext('field', 'Region') ; break;
case 'country' : $fieldName = $this->l10n->pgettext('field', 'Country') ; break;
default: $fieldName = ucfirst($field);
}
} else { } else {
$sql_where = "MATCH (p.`name`, p.`pdesc`, p.`profile_url`, p.`locality`, p.`region`, p.`country`, p.`tags` ) $sql_where = "MATCH (p.`name`, p.`pdesc`, p.`profile_url`, p.`locality`, p.`region`, p.`country`, p.`tags` )
AGAINST (:query IN BOOLEAN MODE)"; AGAINST (:query IN BOOLEAN MODE)";
@ -78,10 +89,12 @@ AND `account_type` = :account_type';
$count = $this->profileModel->getCountForDisplay($sql_where, $values); $count = $this->profileModel->getCountForDisplay($sql_where, $values);
$vars = [ $vars = [
'query' => $originalQuery, 'query' => $originalQuery,
'count' => $count, 'field' => $field,
'accountTypeTabs' => $this->accountTypeTabs->render('search', $account_type, ['q' => $originalQuery]), 'fieldName' => $fieldName,
'profiles' => $profiles, 'count' => $count,
'accountTypeTabs' => $this->accountTypeTabs->render('search', $account_type, ['q' => $originalQuery, 'field' => $field]),
'profiles' => $profiles,
'pager_full' => $pager->renderFull($count), 'pager_full' => $pager->renderFull($count),
'pager_minimal' => $pager->renderMinimal($count), 'pager_minimal' => $pager->renderMinimal($count),
]; ];
@ -89,6 +102,6 @@ AND `account_type` = :account_type';
$content = $this->renderer->fetch('search.phtml', $vars); $content = $this->renderer->fetch('search.phtml', $vars);
// Render index view // Render index view
return $this->renderer->render($response, 'layout.phtml', ['baseUrl' => $request->getUri()->getBaseUrl(), 'content' => $content, 'noNavSearch' => true]); return ['content' => $content, 'noNavSearch' => true];
} }
} }

View file

@ -10,7 +10,7 @@ use Slim\Http\Response;
/** /**
* @author Hypolite Petovan <mrpetovan@gmail.com> * @author Hypolite Petovan <mrpetovan@gmail.com>
*/ */
class Servers class Servers extends BaseController
{ {
/** /**
* @var \Atlas\Pdo\Connection * @var \Atlas\Pdo\Connection
@ -21,7 +21,7 @@ class Servers
*/ */
private $renderer; private $renderer;
/** /**
* @var \Friendica\Directory\Content\L10n * @var \Gettext\TranslatorInterface
*/ */
private $l10n; private $l10n;
/** /**
@ -32,7 +32,7 @@ class Servers
public function __construct( public function __construct(
\Atlas\Pdo\Connection $atlas, \Atlas\Pdo\Connection $atlas,
\Friendica\Directory\Views\PhpRenderer $renderer, \Friendica\Directory\Views\PhpRenderer $renderer,
\Friendica\Directory\Content\L10n $l10n, \Gettext\TranslatorInterface $l10n,
\Psr\SimpleCache\CacheInterface $simplecache \Psr\SimpleCache\CacheInterface $simplecache
) )
{ {
@ -42,7 +42,7 @@ class Servers
$this->simplecache = $simplecache; $this->simplecache = $simplecache;
} }
public function render(Request $request, Response $response): Response public function render(Request $request, Response $response): array
{ {
$stable_version = $this->simplecache->get('stable_version'); $stable_version = $this->simplecache->get('stable_version');
if (!$stable_version) { if (!$stable_version) {
@ -72,7 +72,7 @@ LIMIT :start, :limit';
foreach ($servers as $key => $server) { foreach ($servers as $key => $server) {
$servers[$key]['user_count'] = $this->atlas->fetchValue( $servers[$key]['user_count'] = $this->atlas->fetchValue(
'SELECT COUNT(*) FROM `profile` WHERE `available` AND `server_id` = :server_id', 'SELECT COUNT(*) FROM `profile` WHERE `available` AND NOT `hidden` AND `server_id` = :server_id',
['server_id' => [$server['id'], PDO::PARAM_INT]] ['server_id' => [$server['id'], PDO::PARAM_INT]]
); );
} }
@ -85,7 +85,7 @@ AND NOT `hidden`';
$count = $this->atlas->fetchValue($stmt); $count = $this->atlas->fetchValue($stmt);
$vars = [ $vars = [
'title' => $this->l10n->t('Public Servers'), 'title' => $this->l10n->gettext('Public Servers'),
'servers' => $servers, 'servers' => $servers,
'pager' => $pager->renderFull($count), 'pager' => $pager->renderFull($count),
'stable_version' => $stable_version, 'stable_version' => $stable_version,
@ -95,6 +95,6 @@ AND NOT `hidden`';
$content = $this->renderer->fetch('servers.phtml', $vars); $content = $this->renderer->fetch('servers.phtml', $vars);
// Render index view // Render index view
return $this->renderer->render($response, 'layout.phtml', ['baseUrl' => $request->getUri()->getBaseUrl(), 'content' => $content]); return ['content' => $content];
} }
} }

View file

@ -9,22 +9,10 @@ class Search extends BaseRoute
{ {
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args): \Slim\Http\Response public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args): \Slim\Http\Response
{ {
if ($request->getAttribute('negotiation')->getMediaType() == 'application/json') { return (new \Friendica\Directory\Controllers\Api\Search(
$controller = new \Friendica\Directory\Controllers\Api\Search( $this->container->atlas,
$this->container->atlas, $this->container->get('\Friendica\Directory\Models\Profile'),
$this->container->get('\Friendica\Directory\Models\Profile'), $this->container->l10n
$this->container->l10n ))->render($request, $response, $args);
);
} else {
$controller = new \Friendica\Directory\Controllers\Web\Search(
$this->container->atlas,
$this->container->get('\Friendica\Directory\Models\Profile'),
$this->container->get('\Friendica\Directory\Views\Widget\AccountTypeTabs'),
$this->container->renderer,
$this->container->l10n
);
}
return $controller->render($request, $response, $args);
} }
} }

View file

@ -1,19 +0,0 @@
<?php
namespace Friendica\Directory\Routes\Http;
/**
* @author Hypolite Petovan <mrpetovan@gmail.com>
*/
class Servers extends BaseRoute
{
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args): \Slim\Http\Response
{
return (new \Friendica\Directory\Controllers\Web\Servers(
$this->container->atlas,
$this->container->renderer,
$this->container->l10n,
$this->container->simplecache)
)->render($request, $response);
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Friendica\Directory\Routes\Web;
use Friendica\Directory\Controllers\Web\BaseController;
/**
* @author Hypolite Petovan <mrpetovan@gmail.com>
*/
abstract class BaseRoute
{
/**
* @var \Slim\Container
*/
protected $container;
/**
* @var BaseController
*/
protected $controller;
public function __construct(\Slim\Container $container)
{
$this->container = $container;
}
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args): \Slim\Http\Response
{
$defaults = [
'languages' => $this->container->settings['i18n']['locales'],
'lang' => $request->getAttribute('locale'),
'baseUrl' => $request->getUri()->getBaseUrl(),
'content' => '',
'noNavSearch' => false
];
$values = $this->controller->render($request, $response, $args);
$values = $values + $defaults;
// Render index view
return $this->container->renderer->render($response, 'layout.phtml', $values);
}
}

View file

@ -1,20 +1,22 @@
<?php <?php
namespace Friendica\Directory\Routes\Http; namespace Friendica\Directory\Routes\Web;
/** /**
* @author Hypolite Petovan <mrpetovan@gmail.com> * @author Hypolite Petovan <mrpetovan@gmail.com>
*/ */
class Directory extends BaseRoute class Directory extends BaseRoute
{ {
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args): \Slim\Http\Response public function __construct(\Slim\Container $container)
{ {
return (new \Friendica\Directory\Controllers\Web\Directory( parent::__construct($container);
$this->controller = new \Friendica\Directory\Controllers\Web\Directory(
$this->container->atlas, $this->container->atlas,
$this->container->get('\Friendica\Directory\Models\Profile'), $this->container->get('\Friendica\Directory\Models\Profile'),
$this->container->get('\Friendica\Directory\Views\Widget\AccountTypeTabs'), $this->container->get('\Friendica\Directory\Views\Widget\AccountTypeTabs'),
$this->container->renderer, $this->container->renderer,
$this->container->l10n) $this->container->l10n
)->render($request, $response, $args); );
} }
} }

View file

@ -0,0 +1,22 @@
<?php
namespace Friendica\Directory\Routes\Web;
/**
* @author Hypolite Petovan <mrpetovan@gmail.com>
*/
class Search extends BaseRoute
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->controller = new \Friendica\Directory\Controllers\Web\Search(
$this->container->atlas,
$this->container->get('\Friendica\Directory\Models\Profile'),
$this->container->get('\Friendica\Directory\Views\Widget\AccountTypeTabs'),
$this->container->renderer,
$this->container->l10n
);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Friendica\Directory\Routes\Web;
/**
* @author Hypolite Petovan <mrpetovan@gmail.com>
*/
class Servers extends BaseRoute
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->controller = new \Friendica\Directory\Controllers\Web\Servers(
$this->container->atlas,
$this->container->renderer,
$this->container->l10n,
$this->container->simplecache
);
}
}

165
src/classes/Utils/L10n.php Normal file
View file

@ -0,0 +1,165 @@
<?php
namespace Friendica\Directory\Utils;
use Gettext\Languages\Language;
use Gettext\Translator;
/**
* @author Hypolite Petovan <mrpetovan@gmail.com>
*/
class L10n
{
public static $languages = [
'af' => 'Afrikaans',
'ak' => 'Akan',
'am' => 'አማርኛ',
'ar' => 'العربية',
'as' => 'অসমীয়া',
'az' => 'Azərbaycan',
'be' => 'Беларуская',
'bg' => 'Български',
'bm' => 'Bamanakan',
'bn' => 'বাংলা',
'bo' => 'བོད་སྐད་',
'br' => 'Brezhoneg',
'bs' => 'Bosanski',
'ca' => 'Català',
'cs' => 'Čeština',
'cy' => 'Cymraeg',
'da' => 'Dansk',
'de' => 'Deutsch',
'de_AT' => 'Österreichisches Deutsch',
'de_CH' => 'Schweizer Hochdeutsch',
'dz' => 'རྫོང་ཁ',
'ee' => 'Eʋegbe',
'el' => 'Ελληνικά',
'en' => 'English',
'en_AU' => 'Australian English',
'en_CA' => 'Canadian English',
'en_GB' => 'British English',
'en_US' => 'American English',
'eo' => 'Esperanto',
'es' => 'Español',
'es_ES' => 'Español De España',
'es_MX' => 'Español De México',
'et' => 'Eesti',
'eu' => 'Euskara',
'fa' => 'فارسی',
'ff' => 'Pulaar',
'fi' => 'Suomi',
'fo' => 'Føroyskt',
'fr' => 'Français',
'fr_CA' => 'Français Canadien',
'fr_CH' => 'Français Suisse',
'fy' => 'West-Frysk',
'ga' => 'Gaeilge',
'gd' => 'Gàidhlig',
'gl' => 'Galego',
'gu' => 'ગુજરાતી',
'gv' => 'Gaelg',
'ha' => 'Hausa',
'he' => 'עברית',
'hi' => 'हिन्दी',
'hr' => 'Hrvatski',
'hu' => 'Magyar',
'hy' => 'Հայերեն',
'id' => 'Bahasa Indonesia',
'ig' => 'Igbo',
'ii' => 'ꆈꌠꉙ',
'is' => 'Íslenska',
'it' => 'Italiano',
'ja' => '日本語',
'ka' => 'ქართული',
'ki' => 'Gikuyu',
'kk' => 'Қазақ Тілі',
'kl' => 'Kalaallisut',
'km' => 'ខ្មែរ',
'kn' => 'ಕನ್ನಡ',
'ko' => '한국어',
'ks' => 'کٲشُر',
'kw' => 'Kernewek',
'ky' => 'Кыргызча',
'lb' => 'Lëtzebuergesch',
'lg' => 'Luganda',
'ln' => 'Lingála',
'lo' => 'ລາວ',
'lt' => 'Lietuvių',
'lu' => 'Tshiluba',
'lv' => 'Latviešu',
'mg' => 'Malagasy',
'mk' => 'Македонски',
'ml' => 'മലയാളം',
'mn' => 'Монгол',
'mr' => 'मराठी',
'ms' => 'Bahasa Melayu',
'mt' => 'Malti',
'my' => 'ဗမာ',
'nb' => 'Norsk Bokmål',
'nd' => 'Isindebele',
'ne' => 'नेपाली',
'nl' => 'Nederlands',
'nl_BE' => 'Vlaams',
'nn' => 'Nynorsk',
'no' => 'Norsk',
'om' => 'Oromoo',
'or' => 'ଓଡ଼ିଆ',
'os' => 'Ирон',
'pa' => 'ਪੰਜਾਬੀ',
'pl' => 'Polski',
'ps' => 'پښتو',
'pt' => 'Português',
'pt_BR' => 'Português Do Brasil',
'pt_PT' => 'Português Europeu',
'qu' => 'Runasimi',
'rm' => 'Rumantsch',
'rn' => 'Ikirundi',
'ro' => 'Română',
'ro_MD' => 'Moldovenească',
'ru' => 'Русский',
'rw' => 'Kinyarwanda',
'se' => 'Davvisámegiella',
'sg' => 'Sängö',
'sh' => 'Srpskohrvatski',
'si' => 'සිංහල',
'sk' => 'Slovenčina',
'sl' => 'Slovenščina',
'sn' => 'Chishona',
'so' => 'Soomaali',
'sq' => 'Shqip',
'sr' => 'Српски',
'sv' => 'Svenska',
'sw' => 'Kiswahili',
'ta' => 'தமிழ்',
'te' => 'తెలుగు',
'th' => 'ไทย',
'ti' => 'ትግርኛ',
'tl' => 'Tagalog',
'to' => 'Lea Fakatonga',
'tr' => 'Türkçe',
'ug' => 'ئۇيغۇرچە',
'uk' => 'Українська',
'ur' => 'اردو',
'uz' => 'Oʻzbekcha',
'vi' => 'Tiếng Việt',
'yi' => 'ייִדיש',
'yo' => 'Èdè Yorùbá',
'zh' => '中文',
'zh_Hans' => '简体中文',
'zh_Hant' => '繁體中文',
'zu' => 'Isizulu',
];
public static function langToString($lang)
{
$found = false;
foreach(self::$languages as $key => $language) {
if (strtolower($key) == strtolower(str_replace('-', '_', $lang))) {
$found = true;
break;
}
}
return $found ? $language : $lang;
}
}

View file

@ -6,6 +6,21 @@ namespace Friendica\Directory\Views;
* Zend-Escaper wrapper for Slim PHP Renderer * Zend-Escaper wrapper for Slim PHP Renderer
* *
* @author Hypolite Petovan <mrpetovan@gmail.com> * @author Hypolite Petovan <mrpetovan@gmail.com>
*
* @method string escapeHtml(string $value)
* @method string escapeHtmlAttr(string $value)
* @method string escapeCss(string $value)
* @method string escapeJs(string $value)
* @method string escapeUrl(string $value)
* @method string noop(string $original)
* @method string gettext(string $original)
* @method string ngettext(string $original, string $plural, string $value)
* @method string dngettext(string $domain, string $original, string $plural, string $value)
* @method string npgettext(string $context, string $original, string $plural, string $value)
* @method string pgettext(string $context, string $original)
* @method string dgettext(string $domain, string $original)
* @method string dpgettext(string $domain, string $context, string $original)
* @method string dnpgettext(string $domain, string $context, string $original, string $plural, string $value)
*/ */
class PhpRenderer extends \Slim\Views\PhpRenderer class PhpRenderer extends \Slim\Views\PhpRenderer
{ {
@ -14,13 +29,13 @@ class PhpRenderer extends \Slim\Views\PhpRenderer
*/ */
private $escaper; private $escaper;
/** /**
* @var \Friendica\Directory\Content\L10n * @var \Gettext\TranslatorInterface
*/ */
private $l10n; private $l10n;
public function __construct( public function __construct(
\Zend\Escaper\Escaper $escaper, \Zend\Escaper\Escaper $escaper,
\Friendica\Directory\Content\L10n $l10n, \Gettext\TranslatorInterface $l10n,
string $templatePath = "", string $templatePath = "",
array $attributes = array() array $attributes = array()
) )
@ -36,28 +51,106 @@ class PhpRenderer extends \Slim\Views\PhpRenderer
return $this->escapeHtml($value); return $this->escapeHtml($value);
} }
public function escapeHtml(string $value): string public function __call($name, $arguments)
{ {
return $this->escaper->escapeHtml($value); if (method_exists($this->escaper, $name)) {
return $this->escaper->$name(...$arguments);
} elseif (method_exists($this->l10n, $name)) {
return $this->l10n->$name(...$arguments);
} else {
throw new \Exception('Unknown PhpRendere magic method: ' . $name);
}
} }
public function escapeCss(string $value): string /**
* Echoes the translation of a string.
*
* Loose copy of Gettext/gettext global __() function
*
* Usages:
* - $this->__('Label')
* - $this->__('Label %s', $value)
*
* @param $original
* @param array $args
*
* @return string
*/
public function __($original, ...$args)
{ {
return $this->escaper->escapeCss($value); $text = $this->l10n->gettext($original);
if (!count($args)) {
return $text;
}
return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args);
} }
public function escapeJs(string $value): string /**
* Returns the translation of a string in a specific context.
*
* @param string $context
* @param string $original
*
* @param array $args
* @return string
*/
function p__($context, $original, ...$args)
{ {
return $this->escaper->escapeJs($value); $text = $this->l10n->pgettext($context, $original);
if (!count($args)) {
return $text;
}
return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args);
} }
public function escapeHtmlAttr(string $value): string /**
* Returns the translation of a string in a specific domain.
*
* @param string $domain
* @param string $original
*
* @param array $args
* @return string
*/
function d__($domain, $original, ...$args)
{ {
return $this->escaper->escapeHtmlAttr($value); $text = $this->l10n->dgettext($domain, $original);
if (!count($args)) {
return $text;
}
return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args);
} }
public function escapeUrl(string $value): string /**
* Echoes the singular/plural translation of a string.
*
* Loose copy of Gettext/gettext global n__() function
*
* Usages:
* - $this->n__('Label', 'Labels', 3)
* - $this->n__('%d Label for %s', '%d Labels for %s', 3, $value)
*
* @param string $original
* @param string $plural
* @param string $count
* @param array $args
*
* @return string
*/
function n__($original, $plural, $count, ...$args)
{ {
return $this->escaper->escapeUrl($value); $text = $this->l10n->ngettext($original, $plural, $count);
array_unshift($args, $count);
return is_array($args[1]) ? strtr($text, $args[1]) : vsprintf($text, $args);
} }
} }

View file

@ -0,0 +1,36 @@
<?php
namespace Friendica\Directory\Views\Widget;
/**
* @author Hypolite Petovan <mrpetovan@gmail.com>
*/
class PopularLanguages
{
/**
* @var \Atlas\Pdo\Connection
*/
private $connection;
/**
* @var \Friendica\Directory\Views\PhpRenderer
*/
private $renderer;
public function __construct(\Atlas\Pdo\Connection $connection, \Friendica\Directory\Views\PhpRenderer $renderer)
{
$this->connection = $connection;
$this->renderer = $renderer;
}
public function render(): string
{
$stmt = 'SELECT `language`, COUNT(*) AS `total` FROM `profile` WHERE `language` IS NOT NULL GROUP BY `language` ORDER BY COUNT(`language`) DESC LIMIT 10';
$languages = $this->connection->fetchAll($stmt);
$vars = [
'languages' => $languages
];
return $this->renderer->fetch('widget/popularlanguages.phtml', $vars);
}
}

View file

@ -5,9 +5,9 @@ use Interop\Container\ContainerInterface;
// DIC configuration // DIC configuration
// l10n // l10n
$container['l10n'] = function (ContainerInterface $c): Friendica\Directory\Content\L10n { $container['l10n'] = function (ContainerInterface $c): Gettext\TranslatorInterface {
$settings = $c->get('settings')['l10n']; $translator = new Gettext\Translator();
return new Friendica\Directory\Content\L10n($settings['language'] ?: 'en', $settings['lang_path'] ?: ''); return $translator;
}; };
// simple cache // simple cache

View file

@ -1,9 +1,40 @@
<?php <?php
// Application middleware // Application middleware
// e.g: $app->add(new \Slim\Csrf\Guard); use Boronczyk\LocalizationMiddleware;
// configure middleware
$app->add(new \Gofabian\Negotiation\NegotiationMiddleware([ $app->add(new \Gofabian\Negotiation\NegotiationMiddleware([
'accept' => ['text/html', 'application/json'] 'accept' => ['text/html', 'application/json']
])); ]));
$middleware = new LocalizationMiddleware(
$container->get('settings')['i18n']['locales'],
$container->get('settings')['i18n']['default']
);
$middleware->setLocaleCallback(function (string $locale) use ($container) {
$langPath = $container->get('settings')['i18n']['path'];
$translator = $container->get('l10n');
if (is_a($translator, 'Gettext\GettextTranslator')) {
// One of them will end up working, right?
$translator->setLanguage($locale);
$translator->setLanguage($locale . '.utf8');
$translator->setLanguage($locale . '.UTF8');
$translator->setLanguage($locale . '.utf-8');
$translator->setLanguage($locale . '.UTF-8');
$translator->loadDomain('strings', $langPath);
} else {
/** @var $translator \Gettext\Translator */
if (file_exists($langPath . '/' . $locale . '/LC_MESSAGES/strings.mo')) {
$translator->loadTranslations(Gettext\Translations::fromMoFile($langPath . '/' . $locale . '/LC_MESSAGES/strings.mo'));
} elseif (file_exists($langPath . '/' . $locale . '/LC_MESSAGES/strings.po')) {
$translator->loadTranslations(Gettext\Translations::fromPoFile($langPath . '/' . $locale . '/LC_MESSAGES/strings.po'));
}
}
});
$middleware->setUriParamName('lang');
$app->add($middleware);

View file

@ -9,9 +9,17 @@ use Slim\Http\Response;
* @var $app \Slim\App * @var $app \Slim\App
*/ */
$app->get('/servers', \Friendica\Directory\Routes\Http\Servers::class); $app->get('/servers', \Friendica\Directory\Routes\Web\Servers::class);
$app->get('/search[/{account_type}]', \Friendica\Directory\Routes\Http\Search::class)->setName('search'); $app->get('/search[/{account_type}]', function (Request $request, Response $response, $args) {
if ($request->getAttribute('negotiation')->getMediaType() == 'application/json') {
$route = new \Friendica\Directory\Routes\Http\Search($this);
} else {
$route = new \Friendica\Directory\Routes\Web\Search($this);
}
return $route($request, $response, $args);
})->setName('search');
$app->get('/submit', \Friendica\Directory\Routes\Http\Submit::class); $app->get('/submit', \Friendica\Directory\Routes\Http\Submit::class);
@ -26,4 +34,4 @@ $app->get('/VERSION', function (Request $request, Response $response) {
return $response; return $response;
}); });
$app->get('/[{account_type}]', \Friendica\Directory\Routes\Http\Directory::class)->setName('directory'); $app->get('/[{account_type}]', \Friendica\Directory\Routes\Web\Directory::class)->setName('directory');

View file

@ -18,6 +18,11 @@ if (\is_readable(__DIR__ . '/../config/local.json')) {
$settings = [ $settings = [
'displayErrorDetails' => false, // set to false in production 'displayErrorDetails' => false, // set to false in production
'addContentLengthHeader' => false, // Allow the web server to send the content-length header 'addContentLengthHeader' => false, // Allow the web server to send the content-length header
'i18n' => [
'locales' => ['en', 'fr'],
'default' => 'en',
'path' => __DIR__ . '/lang'
],
// Escaper settings // Escaper settings
'escaper' => [ 'escaper' => [
'encoding' => 'utf-8' 'encoding' => 'utf-8'

View file

@ -0,0 +1,12 @@
<div>
<h3><?php echo $this->__('Popular Languages')?></h3>
<ul>
<?php foreach ($languages as $language): ?>
<li>
<a href="search?field=language&q=<?php echo $this->escapeUrl($language['language']) ?>">
<?php echo $this->e(Friendica\Directory\Utils\L10n::langToString($language['language'])) ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>