Merge branch 'feature/23' into develop

This commit is contained in:
Alejandro Celaya 2016-07-21 17:00:42 +02:00
commit 95346ebb7e
34 changed files with 737 additions and 99 deletions

View File

@ -3,6 +3,7 @@ APP_ENV=
SHORTENED_URL_SCHEMA=
SHORTENED_URL_HOSTNAME=
SHORTCODE_CHARS=
DEFAULT_LOCALE=
# Database
DB_USER=

View File

@ -21,10 +21,11 @@
"zendframework/zend-servicemanager": "^3.0",
"zendframework/zend-paginator": "^2.6",
"zendframework/zend-config": "^2.6",
"zendframework/zend-i18n": "^2.7",
"mtymek/expressive-config-manager": "^0.4",
"acelaya/zsm-annotated-services": "^0.2.0",
"doctrine/orm": "^2.5",
"guzzlehttp/guzzle": "^6.2",
"acelaya/zsm-annotated-services": "^0.2.0",
"symfony/console": "^3.0"
},
"require-dev": {

View File

@ -0,0 +1,8 @@
<?php
return [
'translator' => [
'locale' => getenv('DEFAULT_LOCALE') ?: 'en',
],
];

View File

@ -15,6 +15,12 @@ Statuses:
[TODO]
## Language
In order to set the application language, you have to pass it by using the Accept-Language header.
If not provided or provided language is not supported, english (en_US) will be used.
## Endpoints
#### Authenticate

View File

@ -0,0 +1,14 @@
<?php
return [
'translator' => [
'translation_file_patterns' => [
[
'type' => 'gettext',
'base_dir' => __DIR__ . '/../lang',
'pattern' => '%s.mo',
],
],
],
];

BIN
module/CLI/lang/es.mo Normal file

Binary file not shown.

135
module/CLI/lang/es.po Normal file
View File

@ -0,0 +1,135 @@
msgid ""
msgstr ""
"Project-Id-Version: Shlink 1.0\n"
"POT-Creation-Date: 2016-07-21 15:54+0200\n"
"PO-Revision-Date: 2016-07-21 15:56+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.7.1\n"
"X-Poedit-Basepath: ..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: translate;translatePlural\n"
"X-Poedit-SearchPath-0: src\n"
"X-Poedit-SearchPath-1: config\n"
msgid "Generates a shortcode for provided URL and returns the short URL"
msgstr ""
"Genera un código corto para la URL proporcionada y devuelve la URL acortada"
msgid "The long URL to parse"
msgstr "La URL larga a procesar"
msgid "A long URL was not provided. Which URL do you want to shorten?:"
msgstr "No se ha proporcionado una URL larga. ¿Qué URL deseas acortar?"
msgid "A URL was not provided!"
msgstr "¡No se ha proporcionado una URL!"
msgid "Processed URL:"
msgstr "URL procesada:"
msgid "Generated URL:"
msgstr "URL generada:"
#, php-format
msgid "Provided URL \"%s\" is invalid. Try with a different one."
msgstr "La URL proporcionada \"%s\" e inválida. Prueba con una diferente."
msgid "Returns the detailed visits information for provided short code"
msgstr ""
"Devuelve la información detallada de visitas para el código corto "
"proporcionado"
msgid "The short code which visits we want to get"
msgstr "El código corto del cual queremos obtener las visitas"
msgid "Allows to filter visits, returning only those older than start date"
msgstr ""
"Permite filtrar las visitas, devolviendo sólo aquellas más antiguas que "
"startDate"
msgid "Allows to filter visits, returning only those newer than end date"
msgstr ""
"Permite filtrar las visitas, devolviendo sólo aquellas más nuevas que endDate"
msgid "A short code was not provided. Which short code do you want to use?:"
msgstr "No se prporcionó un código corto. ¿Qué código corto deseas usar?"
msgid "Referer"
msgstr "Origen"
msgid "Date"
msgstr "Fecha"
msgid "Remote Address"
msgstr "Dirección remota"
msgid "User agent"
msgstr "Agente de usuario"
msgid "List all short URLs"
msgstr "Listar todas las URLs cortas"
#, php-format
msgid "The first page to list (%s items per page)"
msgstr "La primera página a listar (%s elementos por página)"
msgid "Short code"
msgstr "Código corto"
msgid "Original URL"
msgstr "URL original"
msgid "Date created"
msgstr "Fecha de creación"
msgid "Visits count"
msgstr "Número de visitas"
msgid "You have reached last page"
msgstr "Has alcanzado la última página"
msgid "Continue with page"
msgstr "Continuar con la página"
msgid "Processes visits where location is not set yet"
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"
msgid "Processing IP"
msgstr "Procesando IP"
msgid "Ignored localhost address"
msgstr "Ignorada IP de localhost"
#, php-format
msgid "Address located at \"%s\""
msgstr "Dirección localizada en \"%s\""
msgid "Finished processing all IPs"
msgstr "Finalizado el procesado de todas las IPs"
msgid "Returns the long URL behind a short code"
msgstr "Devuelve la URL larga detrás de un código corto"
msgid "The short code to parse"
msgstr "El código corto a convertir"
msgid "A short code was not provided. Which short code do you want to parse?:"
msgstr ""
"No se proporcionó un código corto. ¿Qué código corto quieres convertir?"
#, php-format
msgid "No URL found for short code \"%s\""
msgstr "No se ha encontrado ninguna URL para el código corto \"%s\""
msgid "Long URL:"
msgstr "URL larga:"
#, php-format
msgid "Provided short code \"%s\" has an invalid format."
msgstr "El código corto proporcionado \"%s\" tiene un formato inválido."

View File

@ -12,6 +12,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Zend\Diactoros\Uri;
use Zend\I18n\Translator\TranslatorInterface;
class GenerateShortcodeCommand extends Command
{
@ -23,26 +24,37 @@ class GenerateShortcodeCommand extends Command
* @var array
*/
private $domainConfig;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* GenerateShortcodeCommand constructor.
* @param UrlShortenerInterface|UrlShortener $urlShortener
* @param TranslatorInterface $translator
* @param array $domainConfig
*
* @Inject({UrlShortener::class, "config.url_shortener.domain"})
* @Inject({UrlShortener::class, "translator", "config.url_shortener.domain"})
*/
public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig)
{
parent::__construct(null);
public function __construct(
UrlShortenerInterface $urlShortener,
TranslatorInterface $translator,
array $domainConfig
) {
$this->urlShortener = $urlShortener;
$this->translator = $translator;
$this->domainConfig = $domainConfig;
parent::__construct(null);
}
public function configure()
{
$this->setName('shortcode:generate')
->setDescription('Generates a shortcode for provided URL and returns the short URL')
->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse');
->setDescription(
$this->translator->translate('Generates a shortcode for provided URL and returns the short URL')
)
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'));
}
public function interact(InputInterface $input, OutputInterface $output)
@ -54,9 +66,10 @@ class GenerateShortcodeCommand extends Command
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$question = new Question(
'<question>A long URL was not provided. Which URL do you want to shorten?:</question> '
);
$question = new Question(sprintf(
'<question>%s</question> ',
$this->translator->translate('A long URL was not provided. Which URL do you want to shorten?:')
));
$longUrl = $helper->ask($input, $output, $question);
if (! empty($longUrl)) {
@ -70,7 +83,7 @@ class GenerateShortcodeCommand extends Command
try {
if (! isset($longUrl)) {
$output->writeln('<error>A URL was not provided!</error>');
$output->writeln(sprintf('<error>%s</error>', $this->translator->translate('A URL was not provided!')));
return;
}
@ -80,13 +93,16 @@ class GenerateShortcodeCommand extends Command
->withHost($this->domainConfig['hostname']);
$output->writeln([
sprintf('Processed URL <info>%s</info>', $longUrl),
sprintf('Generated URL <info>%s</info>', $shortUrl),
sprintf('%s <info>%s</info>', $this->translator->translate('Processed URL:'), $longUrl),
sprintf('%s <info>%s</info>', $this->translator->translate('Generated URL:'), $shortUrl),
]);
} catch (InvalidUrlException $e) {
$output->writeln(
sprintf('<error>Provided URL "%s" is invalid. Try with a different one.</error>', $longUrl)
);
$output->writeln(sprintf(
'<error>' . $this->translator->translate(
'Provided URL "%s" is invalid. Try with a different one.'
) . '</error>',
$longUrl
));
}
}
}

View File

@ -13,6 +13,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Zend\I18n\Translator\TranslatorInterface;
class GetVisitsCommand extends Command
{
@ -20,35 +21,47 @@ class GetVisitsCommand extends Command
* @var VisitsTrackerInterface
*/
private $visitsTracker;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* GetVisitsCommand constructor.
* @param VisitsTrackerInterface|VisitsTracker $visitsTracker
* @param TranslatorInterface $translator
*
* @Inject({VisitsTracker::class})
* @Inject({VisitsTracker::class, "translator"})
*/
public function __construct(VisitsTrackerInterface $visitsTracker)
public function __construct(VisitsTrackerInterface $visitsTracker, TranslatorInterface $translator)
{
parent::__construct(null);
$this->visitsTracker = $visitsTracker;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName('shortcode:visits')
->setDescription('Returns the detailed visits information for provided short code')
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get')
->setDescription(
$this->translator->translate('Returns the detailed visits information for provided short code')
)
->addArgument(
'shortCode',
InputArgument::REQUIRED,
$this->translator->translate('The short code which visits we want to get')
)
->addOption(
'startDate',
's',
InputOption::VALUE_OPTIONAL,
'Allows to filter visits, returning only those older than start date'
$this->translator->translate('Allows to filter visits, returning only those older than start date')
)
->addOption(
'endDate',
'e',
InputOption::VALUE_OPTIONAL,
'Allows to filter visits, returning only those newer than end date'
$this->translator->translate('Allows to filter visits, returning only those newer than end date')
);
}
@ -61,9 +74,10 @@ class GetVisitsCommand extends Command
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$question = new Question(
'<question>A short code was not provided. Which short code do you want to use?:</question> '
);
$question = new Question(sprintf(
'<question>%s</question> ',
$this->translator->translate('A short code was not provided. Which short code do you want to use?:')
));
$shortCode = $helper->ask($input, $output, $question);
if (! empty($shortCode)) {
@ -80,10 +94,10 @@ class GetVisitsCommand extends Command
$visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate));
$table = new Table($output);
$table->setHeaders([
'Referer',
'Date',
'Remote Address',
'User agent',
$this->translator->translate('Referer'),
$this->translator->translate('Date'),
$this->translator->translate('Remote Address'),
$this->translator->translate('User agent'),
]);
foreach ($visits as $row) {

View File

@ -13,6 +13,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Zend\I18n\Translator\TranslatorInterface;
class ListShortcodesCommand extends Command
{
@ -22,28 +23,37 @@ class ListShortcodesCommand extends Command
* @var ShortUrlServiceInterface
*/
private $shortUrlService;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* ListShortcodesCommand constructor.
* @param ShortUrlServiceInterface|ShortUrlService $shortUrlService
* @param TranslatorInterface $translator
*
* @Inject({ShortUrlService::class})
* @Inject({ShortUrlService::class, "translator"})
*/
public function __construct(ShortUrlServiceInterface $shortUrlService)
public function __construct(ShortUrlServiceInterface $shortUrlService, TranslatorInterface $translator)
{
parent::__construct(null);
$this->shortUrlService = $shortUrlService;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName('shortcode:list')
->setDescription('List all short URLs')
->setDescription($this->translator->translate('List all short URLs'))
->addOption(
'page',
'p',
InputOption::VALUE_OPTIONAL,
sprintf('The first page to list (%s items per page)', PaginableRepositoryAdapter::ITEMS_PER_PAGE),
sprintf(
$this->translator->translate('The first page to list (%s items per page)'),
PaginableRepositoryAdapter::ITEMS_PER_PAGE
),
1
);
}
@ -59,10 +69,10 @@ class ListShortcodesCommand extends Command
$page++;
$table = new Table($output);
$table->setHeaders([
'Short code',
'Original URL',
'Date created',
'Visits count',
$this->translator->translate('Short code'),
$this->translator->translate('Original URL'),
$this->translator->translate('Date created'),
$this->translator->translate('Visits count'),
]);
foreach ($result as $row) {
@ -72,10 +82,14 @@ class ListShortcodesCommand extends Command
if ($this->isLastPage($result)) {
$continue = false;
$output->writeln('<info>You have reached last page</info>');
$output->writeln(
sprintf('<info>%s</info>', $this->translator->translate('You have reached last page'))
);
} else {
$continue = $helper->ask($input, $output, new ConfirmationQuestion(
sprintf('<question>Continue with page <bg=cyan;options=bold>%s</>? (y/N)</question> ', $page),
sprintf('<question>' . $this->translator->translate(
'Continue with page'
) . ' <bg=cyan;options=bold>%s</>? (y/N)</question> ', $page),
false
));
}

View File

@ -11,6 +11,7 @@ use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Zend\I18n\Translator\TranslatorInterface;
class ProcessVisitsCommand extends Command
{
@ -24,25 +25,36 @@ class ProcessVisitsCommand extends Command
* @var IpLocationResolverInterface
*/
private $ipLocationResolver;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* ProcessVisitsCommand constructor.
* @param VisitServiceInterface|VisitService $visitService
* @param IpLocationResolverInterface|IpLocationResolver $ipLocationResolver
* @param TranslatorInterface $translator
*
* @Inject({VisitService::class, IpLocationResolver::class})
* @Inject({VisitService::class, IpLocationResolver::class, "translator"})
*/
public function __construct(VisitServiceInterface $visitService, IpLocationResolverInterface $ipLocationResolver)
{
parent::__construct(null);
public function __construct(
VisitServiceInterface $visitService,
IpLocationResolverInterface $ipLocationResolver,
TranslatorInterface $translator
) {
$this->visitService = $visitService;
$this->ipLocationResolver = $ipLocationResolver;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName('visit:process')
->setDescription('Processes visits where location is not set already');
->setDescription(
$this->translator->translate('Processes visits where location is not set yet')
);
}
public function execute(InputInterface $input, OutputInterface $output)
@ -51,9 +63,11 @@ class ProcessVisitsCommand extends Command
foreach ($visits as $visit) {
$ipAddr = $visit->getRemoteAddr();
$output->write(sprintf('Processing IP <info>%s</info>', $ipAddr));
$output->write(sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
if ($ipAddr === self::LOCALHOST) {
$output->writeln(' (<comment>Ignored localhost address</comment>)');
$output->writeln(
sprintf(' (<comment>%s</comment>)', $this->translator->translate('Ignored localhost address'))
);
continue;
}
@ -63,12 +77,15 @@ class ProcessVisitsCommand extends Command
$location->exchangeArray($result);
$visit->setVisitLocation($location);
$this->visitService->saveVisit($visit);
$output->writeln(sprintf(' (Address located at "%s")', $location->getCityName()));
$output->writeln(sprintf(
' (' . $this->translator->translate('Address located at "%s"') . ')',
$location->getCityName()
));
} catch (WrongIpException $e) {
continue;
}
}
$output->writeln('Finished processing all IPs');
$output->writeln($this->translator->translate('Finished processing all IPs'));
}
}

View File

@ -11,6 +11,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Zend\I18n\Translator\TranslatorInterface;
class ResolveUrlCommand extends Command
{
@ -18,24 +19,34 @@ class ResolveUrlCommand extends Command
* @var UrlShortenerInterface
*/
private $urlShortener;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* ResolveUrlCommand constructor.
* @param UrlShortenerInterface|UrlShortener $urlShortener
* @param TranslatorInterface $translator
*
* @Inject({UrlShortener::class})
* @Inject({UrlShortener::class, "translator"})
*/
public function __construct(UrlShortenerInterface $urlShortener)
public function __construct(UrlShortenerInterface $urlShortener, TranslatorInterface $translator)
{
parent::__construct(null);
$this->urlShortener = $urlShortener;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName('shortcode:parse')
->setDescription('Returns the long URL behind a short code')
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code to parse');
->setDescription($this->translator->translate('Returns the long URL behind a short code'))
->addArgument(
'shortCode',
InputArgument::REQUIRED,
$this->translator->translate('The short code to parse')
);
}
public function interact(InputInterface $input, OutputInterface $output)
@ -47,9 +58,10 @@ class ResolveUrlCommand extends Command
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$question = new Question(
'<question>A short code was not provided. Which short code do you want to parse?:</question> '
);
$question = new Question(sprintf(
'<question>%s</question> ',
$this->translator->translate('A short code was not provided. Which short code do you want to parse?:')
));
$shortCode = $helper->ask($input, $output, $question);
if (! empty($shortCode)) {
@ -64,15 +76,18 @@ class ResolveUrlCommand extends Command
try {
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
if (! isset($longUrl)) {
$output->writeln(sprintf('<error>No URL found for short code "%s"</error>', $shortCode));
$output->writeln(sprintf(
'<error>' . $this->translator->translate('No URL found for short code "%s"') . '</error>',
$shortCode
));
return;
}
$output->writeln(sprintf('Long URL <info>%s</info>', $longUrl));
$output->writeln(sprintf('%s <info>%s</info>', $this->translator->translate('Long URL:'), $longUrl));
} catch (InvalidShortCodeException $e) {
$output->writeln(
sprintf('<error>Provided short code "%s" has an invalid format.</error>', $shortCode)
);
$output->writeln(sprintf('<error>' . $this->translator->translate(
'Provided short code "%s" has an invalid format.'
) . '</error>', $shortCode));
}
}
}

View File

@ -0,0 +1,14 @@
<?php
use Shlinkio\Shlink\Common\Middleware;
return [
'middleware_pipeline' => [
'pre-routing' => [
'middleware' => [
Middleware\LocaleMiddleware::class,
],
'priority' => 5,
],
],
];

View File

@ -4,7 +4,11 @@ use Doctrine\Common\Cache\Cache;
use Doctrine\ORM\EntityManager;
use Shlinkio\Shlink\Common\Factory\CacheFactory;
use Shlinkio\Shlink\Common\Factory\EntityManagerFactory;
use Shlinkio\Shlink\Common\Factory\TranslatorFactory;
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\Factory\InvokableFactory;
return [
@ -15,10 +19,14 @@ return [
GuzzleHttp\Client::class => InvokableFactory::class,
Cache::class => CacheFactory::class,
IpLocationResolver::class => AnnotatedFactory::class,
Translator::class => TranslatorFactory::class,
TranslatorExtension::class => AnnotatedFactory::class,
LocaleMiddleware::class => AnnotatedFactory::class,
],
'aliases' => [
'em' => EntityManager::class,
'httpClient' => GuzzleHttp\Client::class,
'translator' => Translator::class,
AnnotatedFactory::CACHE_SERVICE => Cache::class,
],
],

View File

@ -0,0 +1,12 @@
<?php
use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension;
return [
'twig' => [
'extensions' => [
TranslatorExtension::class,
],
],
];

View File

@ -0,0 +1,30 @@
<?php
namespace Shlinkio\Shlink\Common\Factory;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class TranslatorFactory implements FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when
* creating a service.
* @throws ContainerException if any other error occurs
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$config = $container->get('config');
return Translator::factory(isset($config['translator']) ? $config['translator'] : []);
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace Shlinkio\Shlink\Common\Middleware;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Zend\I18n\Translator\Translator;
use Zend\Stratigility\MiddlewareInterface;
class LocaleMiddleware implements MiddlewareInterface
{
/**
* @var Translator
*/
private $translator;
/**
* LocaleMiddleware constructor.
* @param Translator $translator
*
* @Inject({"translator"})
*/
public function __construct(Translator $translator)
{
$this->translator = $translator;
}
/**
* Process an incoming request and/or response.
*
* Accepts a server-side request and a response instance, and does
* something with them.
*
* If the response is not complete and/or further processing would not
* interfere with the work done in the middleware, or if the middleware
* wants to delegate to another process, it can use the `$out` callable
* if present.
*
* If the middleware does not return a value, execution of the current
* request is considered complete, and the response instance provided will
* be considered the response to return.
*
* Alternately, the middleware may return a response instance.
*
* Often, middleware will `return $out();`, with the assumption that a
* later middleware will return a response.
*
* @param Request $request
* @param Response $response
* @param null|callable $out
* @return null|Response
*/
public function __invoke(Request $request, Response $response, callable $out = null)
{
if (! $request->hasHeader('Accept-Language')) {
return $out($request, $response);
}
$locale = $request->getHeaderLine('Accept-Language');
$this->translator->setLocale($this->normalizeLocale($locale));
return $out($request, $response);
}
/**
* @param string $locale
* @return string
*/
protected function normalizeLocale($locale)
{
$parts = explode('_', $locale);
if (count($parts) > 1) {
return $parts[0];
}
$parts = explode('-', $locale);
if (count($parts) > 1) {
return $parts[0];
}
return $locale;
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace Shlinkio\Shlink\Common\Twig\Extension;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Zend\I18n\Translator\TranslatorInterface;
class TranslatorExtension extends \Twig_Extension implements TranslatorInterface
{
/**
* @var TranslatorInterface
*/
private $translator;
/**
* TranslatorExtension constructor.
* @param TranslatorInterface $translator
*
* @Inject({"translator"})
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* Returns the name of the extension.
*
* @return string The extension name
*/
public function getName()
{
return __CLASS__;
}
public function getFunctions()
{
return [
new \Twig_SimpleFunction('translate', [$this, 'translate']),
new \Twig_SimpleFunction('translate_plural', [$this, 'translatePlural']),
];
}
/**
* Translate a message.
*
* @param string $message
* @param string $textDomain
* @param string $locale
* @return string
*/
public function translate($message, $textDomain = 'default', $locale = null)
{
return $this->translator->translate($message, $textDomain, $locale);
}
/**
* Translate a plural message.
*
* @param string $singular
* @param string $plural
* @param int $number
* @param string $textDomain
* @param string|null $locale
* @return string
*/
public function translatePlural(
$singular,
$plural,
$number,
$textDomain = 'default',
$locale = null
) {
$this->translator->translatePlural($singular, $plural, $number, $textDomain, $locale);
}
}

View File

@ -0,0 +1,14 @@
<?php
return [
'translator' => [
'translation_file_patterns' => [
[
'type' => 'gettext',
'base_dir' => __DIR__ . '/../lang',
'pattern' => '%s.mo',
],
],
],
];

BIN
module/Core/lang/es.mo Normal file

Binary file not shown.

35
module/Core/lang/es.po Normal file
View File

@ -0,0 +1,35 @@
msgid ""
msgstr ""
"Project-Id-Version: Shlink 1.0\n"
"POT-Creation-Date: 2016-07-21 16:50+0200\n"
"PO-Revision-Date: 2016-07-21 16:51+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.7.1\n"
"X-Poedit-Basepath: ..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: translate;translaePlural;translate_plural\n"
"X-Poedit-SearchPath-0: templates\n"
"X-Poedit-SearchPath-1: config\n"
"X-Poedit-SearchPath-2: src\n"
msgid "Make sure you included all the characters, with no extra punctuation."
msgstr "Asegúrate de haber incluído todos los caracteres, sin puntuación extra."
msgid "Oops!"
msgstr "¡Vaya!"
msgid "This short URL doesn't seem to be valid."
msgstr "Esta URL acortada no parece ser válida."
msgid "URL Not Found"
msgstr "URL no encontrada"
#, php-format
msgid "We encountered a %s %s error."
msgstr "Hemos encontrado un error %s %s."

View File

@ -1,6 +1,6 @@
{% extends 'core/layout/default.html.twig' %}
{% block title %}URL Not Found{% endblock %}
{% block title %}{{ translate('URL Not Found') }}{% endblock %}
{% block stylesheets %}
<style>
@ -10,8 +10,8 @@
{% endblock %}
{% block content %}
<h1>Oops!</h1>
<h1>{{ translate('Oops!') }}</h1>
<hr>
<p>This short URL doesn't seem to be valid.</p>
<p>Make sure you included all the characters, with no extra punctuation.</p>
<p>{{ translate('This short URL doesn\'t seem to be valid.') }}</p>
<p>{{ translate('Make sure you included all the characters, with no extra punctuation.') }}</p>
{% endblock %}

View File

@ -10,11 +10,11 @@
{% endblock %}
{% block content %}
<h1>Oops!</h1>
<h1>{{ translate('Oops!') }}</h1>
<hr>
<p>We encountered a {{ status }} {{ reason }} error.</p>
<p>{{ translate('We encountered a %s %s error.') | format(status, reason) }}</p>
{% if status == 404 %}
<p>This short URL doesn't seem to be valid.</p>
<p>Make sure you included all the characters, with no extra punctuation.</p>
<p>{{ translate('This short URL doesn\'t seem to be valid.') }}</p>
<p>{{ translate('Make sure you included all the characters, with no extra punctuation.') }}</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,14 @@
<?php
return [
'translator' => [
'translation_file_patterns' => [
[
'type' => 'gettext',
'base_dir' => __DIR__ . '/../lang',
'pattern' => '%s.mo',
],
],
],
];

BIN
module/Rest/lang/es.mo Normal file

Binary file not shown.

60
module/Rest/lang/es.po Normal file
View File

@ -0,0 +1,60 @@
msgid ""
msgstr ""
"Project-Id-Version: Shlink 1.0\n"
"POT-Creation-Date: 2016-07-21 16:39+0200\n"
"PO-Revision-Date: 2016-07-21 16:40+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.7.1\n"
"X-Poedit-Basepath: ..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: translate;translatePlural\n"
"X-Poedit-SearchPath-0: config\n"
"X-Poedit-SearchPath-1: src\n"
msgid "You have to provide both \"username\" and \"password\""
msgstr "Debes proporcionar tanto \"username\" como \"password\""
msgid "Invalid username and/or password"
msgstr "Usuario y/o contraseña no válidos"
msgid "A URL was not provided"
msgstr "No se ha proporcionado una URL"
#, php-format
msgid "Provided URL \"%s\" is invalid. Try with a different one."
msgstr ""
"La URL \"%s\" proporcionada es inválida. Inténtalo de nuevo con una "
"diferente."
msgid "Unexpected error occurred"
msgstr "Ocurrió un error inesperado"
#, php-format
msgid "Provided short code \"%s\" is invalid"
msgstr "El código corto \"%s\" proporcionado es inválido"
#, php-format
msgid "No URL found for shortcode \"%s\""
msgstr "No se ha encontrado una URL para el código corto \"%s\""
#, php-format
msgid "Provided short code \"%s\" has an invalid format"
msgstr "El código corto proporcionado \"%s\" tiene un formato no inválido"
#, php-format
msgid ""
"Missing or invalid auth token provided. Perform a new authentication request "
"and send provided token on every new request on the \"%s\" header"
msgstr ""
"No se ha proporcionado token de autenticación o este es inválido. Realia una "
"nueva petición de autenticación y envía el token proporcionado en cada nueva "
"petición en la cabecera \"%s\""
#~ msgid "RestToken not found for token \"%s\""
#~ msgstr "No se ha encontrado un RestToken para el token \"%s\""

View File

@ -9,6 +9,7 @@ use Shlinkio\Shlink\Rest\Service\RestTokenService;
use Shlinkio\Shlink\Rest\Service\RestTokenServiceInterface;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface;
class AuthenticateMiddleware extends AbstractRestMiddleware
{
@ -16,16 +17,22 @@ class AuthenticateMiddleware extends AbstractRestMiddleware
* @var RestTokenServiceInterface
*/
private $restTokenService;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* AuthenticateMiddleware constructor.
* @param RestTokenServiceInterface|RestTokenService $restTokenService
* @param TranslatorInterface $translator
*
* @Inject({RestTokenService::class})
* @Inject({RestTokenService::class, "translator"})
*/
public function __construct(RestTokenServiceInterface $restTokenService)
public function __construct(RestTokenServiceInterface $restTokenService, TranslatorInterface $translator)
{
$this->restTokenService = $restTokenService;
$this->translator = $translator;
}
/**
@ -40,7 +47,7 @@ class AuthenticateMiddleware extends AbstractRestMiddleware
if (! isset($authData['username'], $authData['password'])) {
return new JsonResponse([
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
'message' => 'You have to provide both "username" and "password"'
'message' => $this->translator->translate('You have to provide both "username" and "password"'),
], 400);
}
@ -50,7 +57,7 @@ class AuthenticateMiddleware extends AbstractRestMiddleware
} catch (AuthenticationException $e) {
return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => 'Invalid username and/or password',
'message' => $this->translator->translate('Invalid username and/or password'),
], 401);
}
}

View File

@ -10,6 +10,7 @@ use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\Uri;
use Zend\I18n\Translator\TranslatorInterface;
class CreateShortcodeMiddleware extends AbstractRestMiddleware
{
@ -21,18 +22,27 @@ class CreateShortcodeMiddleware extends AbstractRestMiddleware
* @var array
*/
private $domainConfig;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* GenerateShortcodeMiddleware constructor.
*
* @param UrlShortenerInterface|UrlShortener $urlShortener
* @param TranslatorInterface $translator
* @param array $domainConfig
*
* @Inject({UrlShortener::class, "config.url_shortener.domain"})
* @Inject({UrlShortener::class, "translator", "config.url_shortener.domain"})
*/
public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig)
{
public function __construct(
UrlShortenerInterface $urlShortener,
TranslatorInterface $translator,
array $domainConfig
) {
$this->urlShortener = $urlShortener;
$this->translator = $translator;
$this->domainConfig = $domainConfig;
}
@ -48,7 +58,7 @@ class CreateShortcodeMiddleware extends AbstractRestMiddleware
if (! isset($postData['longUrl'])) {
return new JsonResponse([
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
'message' => 'A URL was not provided',
'message' => $this->translator->translate('A URL was not provided'),
], 400);
}
$longUrl = $postData['longUrl'];
@ -67,12 +77,15 @@ class CreateShortcodeMiddleware extends AbstractRestMiddleware
} catch (InvalidUrlException $e) {
return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl),
'message' => sprintf(
$this->translator->translate('Provided URL "%s" is invalid. Try with a different one.'),
$longUrl
),
], 400);
} catch (\Exception $e) {
return new JsonResponse([
'error' => RestUtils::UNKNOWN_ERROR,
'message' => 'Unexpected error occured',
'message' => $this->translator->translate('Unexpected error occurred'),
], 500);
}
}

View File

@ -10,6 +10,7 @@ use Shlinkio\Shlink\Core\Service\VisitsTracker;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface;
class GetVisitsMiddleware extends AbstractRestMiddleware
{
@ -17,16 +18,22 @@ class GetVisitsMiddleware extends AbstractRestMiddleware
* @var VisitsTrackerInterface
*/
private $visitsTracker;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* GetVisitsMiddleware constructor.
* @param VisitsTrackerInterface|VisitsTracker $visitsTracker
* @param TranslatorInterface $translator
*
* @Inject({VisitsTracker::class})
* @Inject({VisitsTracker::class, "translator"})
*/
public function __construct(VisitsTrackerInterface $visitsTracker)
public function __construct(VisitsTrackerInterface $visitsTracker, TranslatorInterface $translator)
{
$this->visitsTracker = $visitsTracker;
$this->translator = $translator;
}
/**
@ -52,12 +59,12 @@ class GetVisitsMiddleware extends AbstractRestMiddleware
} catch (InvalidArgumentException $e) {
return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => sprintf('Provided short code "%s" is invalid', $shortCode),
'message' => sprintf($this->translator->translate('Provided short code "%s" is invalid'), $shortCode),
], 400);
} catch (\Exception $e) {
return new JsonResponse([
'error' => RestUtils::UNKNOWN_ERROR,
'message' => 'Unexpected error occured',
'message' => $this->translator->translate('Unexpected error occurred'),
], 500);
}
}

View File

@ -9,6 +9,7 @@ use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface;
class ListShortcodesMiddleware extends AbstractRestMiddleware
{
@ -18,16 +19,22 @@ class ListShortcodesMiddleware extends AbstractRestMiddleware
* @var ShortUrlServiceInterface
*/
private $shortUrlService;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* ListShortcodesMiddleware constructor.
* @param ShortUrlServiceInterface|ShortUrlService $shortUrlService
* @param TranslatorInterface $translator
*
* @Inject({ShortUrlService::class})
* @Inject({ShortUrlService::class, "translator"})
*/
public function __construct(ShortUrlServiceInterface $shortUrlService)
public function __construct(ShortUrlServiceInterface $shortUrlService, TranslatorInterface $translator)
{
$this->shortUrlService = $shortUrlService;
$this->translator = $translator;
}
/**
@ -45,7 +52,7 @@ class ListShortcodesMiddleware extends AbstractRestMiddleware
} catch (\Exception $e) {
return new JsonResponse([
'error' => RestUtils::UNKNOWN_ERROR,
'message' => 'Unexpected error occured',
'message' => $this->translator->translate('Unexpected error occurred'),
], 500);
}
}

View File

@ -9,6 +9,7 @@ use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface;
class ResolveUrlMiddleware extends AbstractRestMiddleware
{
@ -16,16 +17,22 @@ class ResolveUrlMiddleware extends AbstractRestMiddleware
* @var UrlShortenerInterface
*/
private $urlShortener;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* ResolveUrlMiddleware constructor.
* @param UrlShortenerInterface|UrlShortener $urlShortener
* @param TranslatorInterface $translator
*
* @Inject({UrlShortener::class})
* @Inject({UrlShortener::class, "translator"})
*/
public function __construct(UrlShortenerInterface $urlShortener)
public function __construct(UrlShortenerInterface $urlShortener, TranslatorInterface $translator)
{
$this->urlShortener = $urlShortener;
$this->translator = $translator;
}
/**
@ -43,7 +50,7 @@ class ResolveUrlMiddleware extends AbstractRestMiddleware
if (! isset($longUrl)) {
return new JsonResponse([
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
'message' => sprintf('No URL found for shortcode "%s"', $shortCode),
'message' => sprintf($this->translator->translate('No URL found for shortcode "%s"'), $shortCode),
], 400);
}
@ -53,12 +60,15 @@ class ResolveUrlMiddleware extends AbstractRestMiddleware
} catch (InvalidShortCodeException $e) {
return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => sprintf('Provided short code "%s" has an invalid format', $shortCode),
'message' => sprintf(
$this->translator->translate('Provided short code "%s" has an invalid format'),
$shortCode
),
], 400);
} catch (\Exception $e) {
return new JsonResponse([
'error' => RestUtils::UNKNOWN_ERROR,
'message' => 'Unexpected error occured',
'message' => $this->translator->translate('Unexpected error occurred'),
], 500);
}
}

View File

@ -10,6 +10,7 @@ use Shlinkio\Shlink\Rest\Service\RestTokenServiceInterface;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Router\RouteResult;
use Zend\I18n\Translator\TranslatorInterface;
use Zend\Stratigility\MiddlewareInterface;
class CheckAuthenticationMiddleware implements MiddlewareInterface
@ -20,16 +21,22 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface
* @var RestTokenServiceInterface
*/
private $restTokenService;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* CheckAuthenticationMiddleware constructor.
* @param RestTokenServiceInterface|RestTokenService $restTokenService
* @param TranslatorInterface $translator
*
* @Inject({RestTokenService::class})
* @Inject({RestTokenService::class, "translator"})
*/
public function __construct(RestTokenServiceInterface $restTokenService)
public function __construct(RestTokenServiceInterface $restTokenService, TranslatorInterface $translator)
{
$this->restTokenService = $restTokenService;
$this->translator = $translator;
}
/**
@ -93,8 +100,10 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface
return new JsonResponse([
'error' => RestUtils::INVALID_AUTH_TOKEN_ERROR,
'message' => sprintf(
'Missing or invalid auth token provided. Perform a new authentication request and send provided token '
. 'on every new request on the "%s" header',
$this->translator->translate(
'Missing or invalid auth token provided. Perform a new authentication request and send provided '
. 'token on every new request on the "%s" header'
),
self::AUTH_TOKEN_HEADER
),
], 401);

View File

@ -21,8 +21,8 @@ class RestTokenService implements RestTokenServiceInterface
/**
* ShortUrlService constructor.
* @param EntityManagerInterface $em
*
* @param array $restConfig
*
* @Inject({"em", "config.rest"})
*/
public function __construct(EntityManagerInterface $em, array $restConfig)

View File

@ -21,7 +21,7 @@ class RestUtils
return self::INVALID_SHORTCODE_ERROR;
case $e instanceof Core\InvalidUrlException:
return self::INVALID_URL_ERROR;
case $e instanceof Core\InvalidArgumentException:
case $e instanceof Common\InvalidArgumentException:
return self::INVALID_ARGUMENT_ERROR;
case $e instanceof Rest\AuthenticationException:
return self::INVALID_CREDENTIALS_ERROR;