diff --git a/.env.dist b/.env.dist
index d6271d57..726635ba 100644
--- a/.env.dist
+++ b/.env.dist
@@ -3,6 +3,7 @@ APP_ENV=
SHORTENED_URL_SCHEMA=
SHORTENED_URL_HOSTNAME=
SHORTCODE_CHARS=
+DEFAULT_LOCALE=
# Database
DB_USER=
diff --git a/composer.json b/composer.json
index 0211ed05..b9be6c6d 100644
--- a/composer.json
+++ b/composer.json
@@ -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": {
diff --git a/config/autoload/translator.global.php b/config/autoload/translator.global.php
new file mode 100644
index 00000000..e3730927
--- /dev/null
+++ b/config/autoload/translator.global.php
@@ -0,0 +1,8 @@
+ [
+ 'locale' => getenv('DEFAULT_LOCALE') ?: 'en',
+ ],
+
+];
diff --git a/data/docs/rest.md b/data/docs/rest.md
index 09147518..35f18e2a 100644
--- a/data/docs/rest.md
+++ b/data/docs/rest.md
@@ -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
diff --git a/module/CLI/config/translator.config.php b/module/CLI/config/translator.config.php
new file mode 100644
index 00000000..ae120db3
--- /dev/null
+++ b/module/CLI/config/translator.config.php
@@ -0,0 +1,14 @@
+ [
+ 'translation_file_patterns' => [
+ [
+ 'type' => 'gettext',
+ 'base_dir' => __DIR__ . '/../lang',
+ 'pattern' => '%s.mo',
+ ],
+ ],
+ ],
+
+];
diff --git a/module/CLI/lang/es.mo b/module/CLI/lang/es.mo
new file mode 100644
index 00000000..5d5830b3
Binary files /dev/null and b/module/CLI/lang/es.mo differ
diff --git a/module/CLI/lang/es.po b/module/CLI/lang/es.po
new file mode 100644
index 00000000..b8fd2f7a
--- /dev/null
+++ b/module/CLI/lang/es.po
@@ -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."
diff --git a/module/CLI/src/Command/GenerateShortcodeCommand.php b/module/CLI/src/Command/GenerateShortcodeCommand.php
index 91cbd228..0a0af9eb 100644
--- a/module/CLI/src/Command/GenerateShortcodeCommand.php
+++ b/module/CLI/src/Command/GenerateShortcodeCommand.php
@@ -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(
- 'A long URL was not provided. Which URL do you want to shorten?: '
- );
+ $question = new Question(sprintf(
+ '%s ',
+ $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('A URL was not provided!');
+ $output->writeln(sprintf('%s', $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 %s', $longUrl),
- sprintf('Generated URL %s', $shortUrl),
+ sprintf('%s %s', $this->translator->translate('Processed URL:'), $longUrl),
+ sprintf('%s %s', $this->translator->translate('Generated URL:'), $shortUrl),
]);
} catch (InvalidUrlException $e) {
- $output->writeln(
- sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl)
- );
+ $output->writeln(sprintf(
+ '' . $this->translator->translate(
+ 'Provided URL "%s" is invalid. Try with a different one.'
+ ) . '',
+ $longUrl
+ ));
}
}
}
diff --git a/module/CLI/src/Command/GetVisitsCommand.php b/module/CLI/src/Command/GetVisitsCommand.php
index e7221e5e..c25ba27a 100644
--- a/module/CLI/src/Command/GetVisitsCommand.php
+++ b/module/CLI/src/Command/GetVisitsCommand.php
@@ -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(
- 'A short code was not provided. Which short code do you want to use?: '
- );
+ $question = new Question(sprintf(
+ '%s ',
+ $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) {
diff --git a/module/CLI/src/Command/ListShortcodesCommand.php b/module/CLI/src/Command/ListShortcodesCommand.php
index 1db1f3fb..e59aa903 100644
--- a/module/CLI/src/Command/ListShortcodesCommand.php
+++ b/module/CLI/src/Command/ListShortcodesCommand.php
@@ -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('You have reached last page');
+ $output->writeln(
+ sprintf('%s', $this->translator->translate('You have reached last page'))
+ );
} else {
$continue = $helper->ask($input, $output, new ConfirmationQuestion(
- sprintf('Continue with page %s>? (y/N) ', $page),
+ sprintf('' . $this->translator->translate(
+ 'Continue with page'
+ ) . ' %s>? (y/N) ', $page),
false
));
}
diff --git a/module/CLI/src/Command/ProcessVisitsCommand.php b/module/CLI/src/Command/ProcessVisitsCommand.php
index 19692e85..e9f95a7b 100644
--- a/module/CLI/src/Command/ProcessVisitsCommand.php
+++ b/module/CLI/src/Command/ProcessVisitsCommand.php
@@ -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 %s', $ipAddr));
+ $output->write(sprintf('%s %s', $this->translator->translate('Processing IP'), $ipAddr));
if ($ipAddr === self::LOCALHOST) {
- $output->writeln(' (Ignored localhost address)');
+ $output->writeln(
+ sprintf(' (%s)', $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'));
}
}
diff --git a/module/CLI/src/Command/ResolveUrlCommand.php b/module/CLI/src/Command/ResolveUrlCommand.php
index 01dad903..62d81f41 100644
--- a/module/CLI/src/Command/ResolveUrlCommand.php
+++ b/module/CLI/src/Command/ResolveUrlCommand.php
@@ -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(
- 'A short code was not provided. Which short code do you want to parse?: '
- );
+ $question = new Question(sprintf(
+ '%s ',
+ $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('No URL found for short code "%s"', $shortCode));
+ $output->writeln(sprintf(
+ '' . $this->translator->translate('No URL found for short code "%s"') . '',
+ $shortCode
+ ));
return;
}
- $output->writeln(sprintf('Long URL %s', $longUrl));
+ $output->writeln(sprintf('%s %s', $this->translator->translate('Long URL:'), $longUrl));
} catch (InvalidShortCodeException $e) {
- $output->writeln(
- sprintf('Provided short code "%s" has an invalid format.', $shortCode)
- );
+ $output->writeln(sprintf('' . $this->translator->translate(
+ 'Provided short code "%s" has an invalid format.'
+ ) . '', $shortCode));
}
}
}
diff --git a/module/Common/config/middleware-pipeline.config.php b/module/Common/config/middleware-pipeline.config.php
new file mode 100644
index 00000000..361621c6
--- /dev/null
+++ b/module/Common/config/middleware-pipeline.config.php
@@ -0,0 +1,14 @@
+ [
+ 'pre-routing' => [
+ 'middleware' => [
+ Middleware\LocaleMiddleware::class,
+ ],
+ 'priority' => 5,
+ ],
+ ],
+];
diff --git a/module/Common/config/services.config.php b/module/Common/config/services.config.php
index 838b7896..42d9dbc5 100644
--- a/module/Common/config/services.config.php
+++ b/module/Common/config/services.config.php
@@ -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,
],
],
diff --git a/module/Common/config/templates.config.php b/module/Common/config/templates.config.php
new file mode 100644
index 00000000..903c1e8c
--- /dev/null
+++ b/module/Common/config/templates.config.php
@@ -0,0 +1,12 @@
+ [
+ 'extensions' => [
+ TranslatorExtension::class,
+ ],
+ ],
+
+];
diff --git a/module/Common/src/Factory/TranslatorFactory.php b/module/Common/src/Factory/TranslatorFactory.php
new file mode 100644
index 00000000..e41ba2fc
--- /dev/null
+++ b/module/Common/src/Factory/TranslatorFactory.php
@@ -0,0 +1,30 @@
+get('config');
+ return Translator::factory(isset($config['translator']) ? $config['translator'] : []);
+ }
+}
diff --git a/module/Common/src/Middleware/LocaleMiddleware.php b/module/Common/src/Middleware/LocaleMiddleware.php
new file mode 100644
index 00000000..20f796ff
--- /dev/null
+++ b/module/Common/src/Middleware/LocaleMiddleware.php
@@ -0,0 +1,82 @@
+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;
+ }
+}
diff --git a/module/Common/src/Twig/Extension/TranslatorExtension.php b/module/Common/src/Twig/Extension/TranslatorExtension.php
new file mode 100644
index 00000000..48ee3f11
--- /dev/null
+++ b/module/Common/src/Twig/Extension/TranslatorExtension.php
@@ -0,0 +1,75 @@
+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);
+ }
+}
diff --git a/module/Core/config/translator.config.php b/module/Core/config/translator.config.php
new file mode 100644
index 00000000..ae120db3
--- /dev/null
+++ b/module/Core/config/translator.config.php
@@ -0,0 +1,14 @@
+ [
+ 'translation_file_patterns' => [
+ [
+ 'type' => 'gettext',
+ 'base_dir' => __DIR__ . '/../lang',
+ 'pattern' => '%s.mo',
+ ],
+ ],
+ ],
+
+];
diff --git a/module/Core/lang/es.mo b/module/Core/lang/es.mo
new file mode 100644
index 00000000..d34bb83b
Binary files /dev/null and b/module/Core/lang/es.mo differ
diff --git a/module/Core/lang/es.po b/module/Core/lang/es.po
new file mode 100644
index 00000000..3393f072
--- /dev/null
+++ b/module/Core/lang/es.po
@@ -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."
diff --git a/module/Core/templates/core/error/404.html.twig b/module/Core/templates/core/error/404.html.twig
index 0c28f29d..fe36f047 100644
--- a/module/Core/templates/core/error/404.html.twig
+++ b/module/Core/templates/core/error/404.html.twig
@@ -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 %}