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 %}