From fa230b61da7dcd1bae7f16e53130f50a321b75ab Mon Sep 17 00:00:00 2001 From: Damien Alexandre Date: Sat, 16 Dec 2017 13:25:51 +0100 Subject: [PATCH 001/234] Add a --cache option on translation:download to clear the cache (#139) * Add a --cache option on translation:download to clear the cache * Refactor the cache clearer as a service and better hash generation * Fix CS and missing stuff * Order files and avoid ~ suffix --- Changelog.md | 20 +++++--- Command/DownloadCommand.php | 42 +++++++++++++++- Controller/EditInPlaceController.php | 38 +-------------- Resources/config/services.yml | 4 ++ Service/CacheClearer.php | 73 ++++++++++++++++++++++++++++ 5 files changed, 132 insertions(+), 45 deletions(-) create mode 100644 Service/CacheClearer.php diff --git a/Changelog.md b/Changelog.md index 9ed0ada8..195d0b28 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,27 +2,33 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## Unreleased + +## Added + +- New `--cache` option on the `translation:download` allowing to clear the cache automatically if the downloaded translations have changed. + ## 0.4.0 Major improvements on this version. Be aware that the default format to store translation has changed to XLIFF 2.0. If you -run the extract command you will automatically get updated files. +run the extract command you will automatically get updated files. ### Added - More extractors from `php-translation/extractor` -- Show status after extract command +- Show status after extract command - Added status command - Support for PHPUnit6 - Support for `php-translation/symfony-storage` 0.3.0 - Using dumper and loader from `php-translation/extractor` -- `CatalogeCounter` to show statistics about a catalogue. -- Lots of more tests. Test coverage increased from 27% to 69%. +- `CatalogueCounter` to show statistics about a catalogue +- Lots of more tests. Test coverage increased from 27% to 69% ### Changed - `Importer` returns an `ImportResult` value object - Improved internal management of metadata. Introduced a new `Metadata` model -- Renamed `MetadataAwareMerged` to `ReplaceOperation`, read the class doc for the updated syntax. +- Renamed `MetadataAwareMerged` to `ReplaceOperation`, read the class doc for the updated syntax ### Fixed @@ -31,7 +37,7 @@ run the extract command you will automatically get updated files. ### Removed - Removed `WebUIMessage` and `EditInPlaceMessage`. Use `Message` from `php-translation/common` instead -- Removed metadata related functions from `CatalogueManager` +- Removed metadata related functions from `CatalogueManager` ### Changed @@ -55,7 +61,7 @@ run the extract command you will automatically get updated files. ### Fixed -- When using EditInPlace, we only mark twig filters (`trans` & `transchoice`) as "safe" when EditInPlace in active. +- When using EditInPlace, we only mark twig filters (`trans` & `transchoice`) as "safe" when EditInPlace in active. ## 0.3.3 diff --git a/Command/DownloadCommand.php b/Command/DownloadCommand.php index ffa8dd59..881df632 100644 --- a/Command/DownloadCommand.php +++ b/Command/DownloadCommand.php @@ -13,9 +13,12 @@ use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Finder\Finder; use Translation\Bundle\Service\StorageService; +use Translation\Bundle\Model\Configuration; /** * @author Tobias Nyholm @@ -27,15 +30,50 @@ protected function configure() $this ->setName('translation:download') ->setDescription('Replace local messages with messages from remote') - ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default'); + ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') + ->addOption('cache', null, InputOption::VALUE_NONE, 'Clear the cache if the translations have changed') + ; } protected function execute(InputInterface $input, OutputInterface $output) { $container = $this->getContainer(); $configName = $input->getArgument('configuration'); + /** @var StorageService $storage */ $storage = $container->get('php_translation.storage.'.$configName); - $storage->download(); + /** @var Configuration $configuration */ + $configuration = $this->getContainer()->get('php_translation.configuration.'.$configName); + + if ($input->getOption('cache')) { + $translationsDirectory = $configuration->getOutputDir(); + $md5BeforeDownload = $this->hashDirectory($translationsDirectory); + $storage->download(); + $md5AfterDownload = $this->hashDirectory($translationsDirectory); + + if ($md5BeforeDownload !== $md5AfterDownload) { + $cacheClearer = $this->getContainer()->get('php_translation.cache_clearer'); + $cacheClearer->clearAndWarmUp(); + } + } else { + $storage->download(); + } + } + + private function hashDirectory($directory) + { + if (!is_dir($directory)) { + return false; + } + + $finder = new Finder(); + $finder->files()->in($directory)->notName('/~$/')->sortByName(); + + $hash = hash_init('md5'); + foreach ($finder as $file) { + hash_update_file($hash, $file->getRealPath()); + } + + return hash_final($hash); } } diff --git a/Controller/EditInPlaceController.php b/Controller/EditInPlaceController.php index c64bba8a..f737786f 100644 --- a/Controller/EditInPlaceController.php +++ b/Controller/EditInPlaceController.php @@ -12,7 +12,6 @@ namespace Translation\Bundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; -use Symfony\Component\Finder\Finder; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Translation\Bundle\Exception\MessageValidationException; @@ -45,45 +44,12 @@ public function editAction(Request $request, $configName, $locale) $storage->update($message); } - $this->rebuildTranslations($locale); + $cacheClearer = $this->get('php_translation.cache_clearer'); + $cacheClearer->clearAndWarmUp($locale); return new Response(); } - /** - * Remove the Symfony translation cache and warm it up again. - * - * @param $locale - */ - private function rebuildTranslations($locale) - { - $cacheDir = $this->getParameter('kernel.cache_dir'); - $translationDir = sprintf('%s/translations', $cacheDir); - - $filesystem = $this->get('filesystem'); - $finder = new Finder(); - - if (!is_dir($translationDir)) { - mkdir($translationDir); - } - - if (!is_writable($translationDir)) { - throw new \RuntimeException(sprintf('Unable to write in the "%s" directory', $translationDir)); - } - - // Remove the translations for this locale - $files = $finder->files()->name('*.'.$locale.'.*')->in($translationDir); - foreach ($files as $file) { - $filesystem->remove($file); - } - - // Build them again - $translator = $this->get('translator'); - if (method_exists($translator, 'warmUp')) { - $translator->warmUp($translationDir); - } - } - /** * Get and validate messages from the request. * diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 4cb140f8..7a5d0721 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -25,6 +25,10 @@ services: class: Translation\Bundle\Service\Importer arguments: ["@php_translation.extractor"] + php_translation.cache_clearer: + class: Translation\Bundle\Service\CacheClearer + arguments: ["%kernel.cache_dir%", "@translator", "@filesystem"] + php_translation.local_file_storage.abstract: class: Translation\SymfonyStorage\FileStorage abstract: true diff --git a/Service/CacheClearer.php b/Service/CacheClearer.php new file mode 100644 index 00000000..9e1c3bc7 --- /dev/null +++ b/Service/CacheClearer.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Service; + +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Finder\Finder; +use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; +use Symfony\Component\Translation\TranslatorInterface; + +/** + * A service able to read and clear the Symfony Translation cache. + * + * @author Damien A. + */ +final class CacheClearer +{ + /** + * @var string + */ + private $kernelCacheDir; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var Filesystem + */ + private $filesystem; + + public function __construct($kernelCacheDir, TranslatorInterface $translator, Filesystem $filesystem) + { + $this->kernelCacheDir = $kernelCacheDir; + $this->translator = $translator; + $this->filesystem = $filesystem; + } + + /** + * Remove the Symfony translation cache and warm it up again. + * + * @param string|null $locale Optional filter to clear only one locale. + */ + public function clearAndWarmUp($locale = null) + { + $translationDir = sprintf('%s/translations', $this->kernelCacheDir); + + $finder = new Finder(); + + // Make sure the directory exists + $this->filesystem->mkdir($translationDir); + + // Remove the translations for this locale + $files = $finder->files()->name($locale ? '*.'.$locale.'.*' : '*')->in($translationDir); + foreach ($files as $file) { + $this->filesystem->remove($file); + } + + // Build them again + if ($this->translator instanceof WarmableInterface) { + $this->translator->warmUp($translationDir); + } + } +} From 661523dd9c94b1729c2a72fcde99e46659aff564 Mon Sep 17 00:00:00 2001 From: Mathieu Santostefano Date: Sat, 16 Dec 2017 13:29:27 +0100 Subject: [PATCH 002/234] Fix twig paths for Twig 2.x (#142) --- Controller/SymfonyProfilerController.php | 2 +- Controller/WebUIController.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Controller/SymfonyProfilerController.php b/Controller/SymfonyProfilerController.php index 3efafe36..0c080a99 100644 --- a/Controller/SymfonyProfilerController.php +++ b/Controller/SymfonyProfilerController.php @@ -48,7 +48,7 @@ public function editAction(Request $request, $token) if ($request->isMethod('GET')) { $translation = $storage->syncAndFetchMessage($message->getLocale(), $message->getDomain(), $message->getKey()); - return $this->render('TranslationBundle:SymfonyProfiler:edit.html.twig', [ + return $this->render('@Translation/SymfonyProfiler/edit.html.twig', [ 'message' => $translation, 'key' => $message->getLocale().$message->getDomain().$message->getKey(), ]); diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index aac5d302..89d53a31 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -70,7 +70,7 @@ public function indexAction($configName = null) } } - return $this->render('TranslationBundle:WebUI:index.html.twig', [ + return $this->render('@Translation/WebUI/index.html.twig', [ 'catalogues' => $catalogues, 'catalogueSize' => $catalogueSize, 'maxDomainSize' => $maxDomainSize, @@ -108,7 +108,7 @@ public function showAction($configName, $locale, $domain) return strcmp($a->getKey(), $b->getKey()); }); - return $this->render('TranslationBundle:WebUI:show.html.twig', [ + return $this->render('@Translation/WebUI/show.html.twig', [ 'messages' => $messages, 'domains' => $catalogueManager->getDomains(), 'currentDomain' => $domain, @@ -159,7 +159,7 @@ public function createAction(Request $request, $configName, $locale, $domain) ), $e); } - return $this->render('TranslationBundle:WebUI:create.html.twig', [ + return $this->render('@Translation/WebUI/create.html.twig', [ 'message' => $message, ]); } From fd8dd562eea237069007ac367e5c7344c1e11777 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Sat, 16 Dec 2017 14:33:03 +0200 Subject: [PATCH 003/234] Tweak indentations to 4 spaces as in Symfony recipes (#146) --- Readme.md | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/Readme.md b/Readme.md index 37a3da26..0cb58f6e 100644 --- a/Readme.md +++ b/Readme.md @@ -20,13 +20,13 @@ $ composer require php-translation/symfony-bundle ```php class AppKernel extends Kernel { - public function registerBundles() - { - $bundles = array( - // ... - new Translation\Bundle\TranslationBundle(), + public function registerBundles() + { + $bundles = array( + // ... + new Translation\Bundle\TranslationBundle(), + } } - } } ``` @@ -35,38 +35,38 @@ An example configuration looks like this: ```yaml # config.yml translation: - locales: ["en", "sv"] - symfony_profiler: # must be placed in config_dev.yml - enabled: true - webui: - enabled: true - edit_in_place: - enabled: true - config_name: default # the first one or one of your configs - activator: php_translation.edit_in_place.activator - configs: - app: - dirs: ["%kernel.root_dir%/Resources/views", "%kernel.root_dir%/../src"] - output_dir: "%kernel.root_dir%/Resources/translations" - excluded_names: ["*TestCase.php", "*Test.php"] - excluded_dirs: [cache, data, logs] + locales: ["en", "sv"] + symfony_profiler: # must be placed in config_dev.yml + enabled: true + webui: + enabled: true + edit_in_place: + enabled: true + config_name: default # the first one or one of your configs + activator: php_translation.edit_in_place.activator + configs: + app: + dirs: ["%kernel.root_dir%/Resources/views", "%kernel.root_dir%/../src"] + output_dir: "%kernel.root_dir%/Resources/translations" + excluded_names: ["*TestCase.php", "*Test.php"] + excluded_dirs: [cache, data, logs] ``` ```yaml # routing_dev.yml _translation_webui: - resource: "@TranslationBundle/Resources/config/routing_webui.yml" - prefix: /admin + resource: "@TranslationBundle/Resources/config/routing_webui.yml" + prefix: /admin _translation_profiler: - resource: '@TranslationBundle/Resources/config/routing_symfony_profiler.yml' + resource: '@TranslationBundle/Resources/config/routing_symfony_profiler.yml' ``` ```yaml # routing.yml _translation_edit_in_place: - resource: '@TranslationBundle/Resources/config/routing_edit_in_place.yml' - prefix: /admin + resource: '@TranslationBundle/Resources/config/routing_edit_in_place.yml' + prefix: /admin ``` ## Documentation From beadb00bf48173fd8f902f3d7f44e0258e96516b Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Sat, 16 Dec 2017 14:33:46 +0200 Subject: [PATCH 004/234] Add StyleCI badge to the README (#147) --- Readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Readme.md b/Readme.md index 0cb58f6e..3bc32247 100644 --- a/Readme.md +++ b/Readme.md @@ -6,6 +6,8 @@ [![Quality Score](https://img.shields.io/scrutinizer/g/php-translation/symfony-bundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-translation/symfony-bundle) [![SensioLabsInsight](https://insight.sensiolabs.com/projects/c289ebe2-41c4-429f-afba-de2f905b9bdb/mini.png)](https://insight.sensiolabs.com/projects/c289ebe2-41c4-429f-afba-de2f905b9bdb) [![Total Downloads](https://img.shields.io/packagist/dt/php-translation/symfony-bundle.svg?style=flat-square)](https://packagist.org/packages/php-translation/symfony-bundle) +[![Coding Style](https://styleci.io/repos/75462210/shield)](https://styleci.io/repos/75462210) + **Symfony integration for PHP Translation** From fd5103317126d6e9d89fae93d8d30d81a95f7b0e Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Sat, 16 Dec 2017 14:41:38 +0200 Subject: [PATCH 005/234] Add Yandex translator support (#151) * Allow Yandex translator to be specified * Declare service for Yandex translator * Removed bing --- DependencyInjection/Configuration.php | 2 +- Resources/config/auto_translation.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index c6eec9d1..af75001c 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -182,7 +182,7 @@ private function addAutoTranslateNode(ArrayNodeDefinition $root) ->arrayNode('fallback_translation') ->canBeEnabled() ->children() - ->enumNode('service')->values(['google', 'bing'])->defaultValue('google')->end() + ->enumNode('service')->values(['google', 'yandex'])->defaultValue('google')->end() ->scalarNode('api_key')->defaultNull()->end() ->end() ->end() diff --git a/Resources/config/auto_translation.yml b/Resources/config/auto_translation.yml index 7690e07a..853b74ec 100644 --- a/Resources/config/auto_translation.yml +++ b/Resources/config/auto_translation.yml @@ -19,3 +19,7 @@ services: php_translation.translator_service.google: class: Translation\Translator\Service\GoogleTranslator arguments: ["%php_translation.translator_service.api_key%"] + + php_translation.translator_service.yandex: + class: Translation\Translator\Service\YandexTranslator + arguments: ["%php_translation.translator_service.api_key%"] From d38e64602bb3b966844ef2a079335fed1efbee49 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sun, 17 Dec 2017 13:14:50 +0100 Subject: [PATCH 006/234] Apply fixes from StyleCI (#155) --- Catalogue/CatalogueCounter.php | 4 ++-- Controller/SymfonyProfilerController.php | 6 +++--- DependencyInjection/TranslationExtension.php | 10 +++++----- EventListener/AutoAddMissingTranslations.php | 4 ++-- Model/CatalogueMessage.php | 4 ++-- Model/Metadata.php | 2 +- Service/ConfigurationManager.php | 2 +- Service/StorageService.php | 1 + Tests/Functional/Command/ExtractCommandTest.php | 6 +++--- Translator/FallbackTranslator.php | 2 +- 10 files changed, 21 insertions(+), 20 deletions(-) diff --git a/Catalogue/CatalogueCounter.php b/Catalogue/CatalogueCounter.php index 536861bc..4d31b7b0 100644 --- a/Catalogue/CatalogueCounter.php +++ b/Catalogue/CatalogueCounter.php @@ -69,11 +69,11 @@ public function getCatalogueStatistics(MessageCatalogueInterface $catalogue) foreach ($catalogue->all($domain) as $key => $text) { $metadata = new Metadata($catalogue->getMetadata($key, $domain)); $state = $metadata->getState(); - if ($state === 'new') { + if ('new' === $state) { ++$result[$domain]['new']; } - if ($state === 'obsolete') { + if ('obsolete' === $state) { ++$result[$domain]['obsolete']; } diff --git a/Controller/SymfonyProfilerController.php b/Controller/SymfonyProfilerController.php index 0c080a99..eef04521 100644 --- a/Controller/SymfonyProfilerController.php +++ b/Controller/SymfonyProfilerController.php @@ -78,7 +78,7 @@ public function syncAction(Request $request, $token) $sfMessage = $this->getMessage($request, $token); $message = $storage->syncAndFetchMessage($sfMessage->getLocale(), $sfMessage->getDomain(), $sfMessage->getKey()); - if ($message !== null) { + if (null !== $message) { return new Response($message->getTranslation()); } @@ -165,7 +165,7 @@ private function getMessage(Request $request, $token) } $message = SfProfilerMessage::create($messages[$messageId]); - if ($message->getState() === DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK) { + if (DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK === $message->getState()) { $message->setLocale($profile->getCollector('request')->getLocale()) ->setTranslation(sprintf('[%s]', $message->getTranslation())); } @@ -185,7 +185,7 @@ protected function getSelectedMessages(Request $request, $token) $profiler->disable(); $selected = $request->request->get('selected'); - if (!$selected || count($selected) == 0) { + if (!$selected || 0 == count($selected)) { return []; } diff --git a/DependencyInjection/TranslationExtension.php b/DependencyInjection/TranslationExtension.php index b2072e36..8aaf753f 100644 --- a/DependencyInjection/TranslationExtension.php +++ b/DependencyInjection/TranslationExtension.php @@ -88,7 +88,7 @@ private function handleConfigNode(ContainerBuilder $container, array $config) // $first will be the "default" configuration. $first = null; foreach ($config['configs'] as $name => &$c) { - if ($first === null || $name === 'default') { + if (null === $first || 'default' === $name) { $first = $name; } if (empty($c['project_root'])) { @@ -115,7 +115,7 @@ private function handleConfigNode(ContainerBuilder $container, array $config) } foreach ($c['local_storage'] as $serviceId) { - if ($serviceId !== 'php_translation.local_file_storage.abstract') { + if ('php_translation.local_file_storage.abstract' !== $serviceId) { $storageDefinition->addMethodCall('addLocalStorage', [new Reference($serviceId)]); continue; @@ -129,10 +129,10 @@ private function handleConfigNode(ContainerBuilder $container, array $config) } } - if ($first !== null) { + if (null !== $first) { // Create some aliases for the default storage $container->setAlias('php_translation.storage', 'php_translation.storage.'.$first); - if ($first !== 'default') { + if ('default' !== $first) { $container->setAlias('php_translation.storage.default', 'php_translation.storage.'.$first); } } @@ -172,7 +172,7 @@ private function enableEditInPlace(ContainerBuilder $container, array $config) { $name = $config['edit_in_place']['config_name']; - if ($name !== 'default' && !isset($config['configs'][$name])) { + if ('default' !== $name && !isset($config['configs'][$name])) { throw new InvalidArgumentException(sprintf('There is no config named "%s".', $name)); } diff --git a/EventListener/AutoAddMissingTranslations.php b/EventListener/AutoAddMissingTranslations.php index 3552051f..7f38150b 100644 --- a/EventListener/AutoAddMissingTranslations.php +++ b/EventListener/AutoAddMissingTranslations.php @@ -43,13 +43,13 @@ public function __construct(StorageService $storage, DataCollectorTranslator $tr public function onTerminate(Event $event) { - if ($this->dataCollector === null) { + if (null === $this->dataCollector) { return; } $messages = $this->dataCollector->getCollectedMessages(); foreach ($messages as $message) { - if ($message['state'] === DataCollectorTranslator::MESSAGE_MISSING) { + if (DataCollectorTranslator::MESSAGE_MISSING === $message['state']) { $m = new Message($message['id'], $message['domain'], $message['locale'], $message['translation']); $this->storage->create($m); } diff --git a/Model/CatalogueMessage.php b/Model/CatalogueMessage.php index 36a15738..ed534c0b 100644 --- a/Model/CatalogueMessage.php +++ b/Model/CatalogueMessage.php @@ -135,7 +135,7 @@ public function isNew() return false; } - return $this->metadata->getState() === 'new'; + return 'new' === $this->metadata->getState(); } public function isObsolete() @@ -144,7 +144,7 @@ public function isObsolete() return false; } - return $this->metadata->getState() === 'obsolete'; + return 'obsolete' === $this->metadata->getState(); } public function isApproved() diff --git a/Model/Metadata.php b/Model/Metadata.php index 3fa82ce5..d78f0624 100644 --- a/Model/Metadata.php +++ b/Model/Metadata.php @@ -73,7 +73,7 @@ public function isApproved() $notes = $this->getAllInCategory('approved'); foreach ($notes as $note) { if (isset($note['content'])) { - return $note['content'] === 'true'; + return 'true' === $note['content']; } } diff --git a/Service/ConfigurationManager.php b/Service/ConfigurationManager.php index 41f59da2..e229af1f 100644 --- a/Service/ConfigurationManager.php +++ b/Service/ConfigurationManager.php @@ -49,7 +49,7 @@ public function getConfiguration($name = null) return $this->configuration[$name]; } - if ($name === 'default') { + if ('default' === $name) { $name = $this->getFirstName(); if (isset($this->configuration[$name])) { return $this->configuration[$name]; diff --git a/Service/StorageService.php b/Service/StorageService.php index 62492fdd..5b4a6984 100644 --- a/Service/StorageService.php +++ b/Service/StorageService.php @@ -28,6 +28,7 @@ final class StorageService implements Storage { const DIRECTION_UP = 'up'; + const DIRECTION_DOWN = 'down'; /** diff --git a/Tests/Functional/Command/ExtractCommandTest.php b/Tests/Functional/Command/ExtractCommandTest.php index cd9f4343..35da666a 100644 --- a/Tests/Functional/Command/ExtractCommandTest.php +++ b/Tests/Functional/Command/ExtractCommandTest.php @@ -93,15 +93,15 @@ public function testExecute() // Test meta, source-location $meta = new Metadata($catalogue->getMetadata('translated.paragraph1')); - $this->assertFalse($meta->getState() === 'new'); + $this->assertFalse('new' === $meta->getState()); foreach ($meta->getSourceLocations() as $sourceLocation) { $this->assertNotEquals('foobar.html.twig', $sourceLocation['path']); } $meta = new Metadata($catalogue->getMetadata('not.in.source')); - $this->assertTrue($meta->getState() === 'obsolete'); + $this->assertTrue('obsolete' === $meta->getState()); $meta = new Metadata($catalogue->getMetadata('translated.title')); - $this->assertTrue($meta->getState() === 'new'); + $this->assertTrue('new' === $meta->getState()); } } diff --git a/Translator/FallbackTranslator.php b/Translator/FallbackTranslator.php index c4212b84..a8a6446d 100644 --- a/Translator/FallbackTranslator.php +++ b/Translator/FallbackTranslator.php @@ -151,7 +151,7 @@ private function translateWithSubstitutedParameters($orgString, $locale, array $ $replacedString = str_replace(array_keys($replacements), array_values($replacements), $orgString); $translatedString = $this->externalTranslator->translate($replacedString, $this->defaultLocale, $locale); - if ($translatedString === null) { + if (null === $translatedString) { // Could not be translated return $orgString; } From 0136bbb2e042a52ccb5ac114fad35cca8dd8483a Mon Sep 17 00:00:00 2001 From: Christophe TAVERNE Date: Wed, 20 Dec 2017 12:23:40 +0100 Subject: [PATCH 007/234] Correct JS (#156) Correct JS to make work "Synchronize all translations" --- Resources/public/js/symfonyProfiler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/public/js/symfonyProfiler.js b/Resources/public/js/symfonyProfiler.js index 7067bc8d..1558c039 100644 --- a/Resources/public/js/symfonyProfiler.js +++ b/Resources/public/js/symfonyProfiler.js @@ -40,7 +40,7 @@ function syncMessage(key) { function syncAll() { var el = document.getElementById("top-result-area"); - el[0].innerHTML = getLoaderHTML(); + el.innerHTML = getLoaderHTML(); Sfjs.request( translationSyncAllUrl, From 78587713e619bb9b0f521acae6f53cee6a3ab152 Mon Sep 17 00:00:00 2001 From: Christophe TAVERNE Date: Wed, 20 Dec 2017 12:26:02 +0100 Subject: [PATCH 008/234] Correct translation fallback edition (#158) I solved this error by using message key from query parameter instead of current locale wich are different in case of the fallback translation, contrary to normal edition. --- Controller/SymfonyProfilerController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Controller/SymfonyProfilerController.php b/Controller/SymfonyProfilerController.php index eef04521..8f659f78 100644 --- a/Controller/SymfonyProfilerController.php +++ b/Controller/SymfonyProfilerController.php @@ -50,7 +50,7 @@ public function editAction(Request $request, $token) return $this->render('@Translation/SymfonyProfiler/edit.html.twig', [ 'message' => $translation, - 'key' => $message->getLocale().$message->getDomain().$message->getKey(), + 'key' => $request->query->get('message_id'), ]); } From e6b000b4e30471a19a525a0eb4b32ad9e6367ad5 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Thu, 28 Dec 2017 17:33:23 +0200 Subject: [PATCH 009/234] Add Symfony 4 support (#145) * Add Symfony 4 support * Use ChildDefinition instead of DefinitionDecorator * Make some services public and fix deprecated class calls * Fix more tests * Fix remaining tests: Make more services public * Call write() when exists instead of deprecated writeTranslations() * Make php_translation.cache_clearer service public - we call it in controller * Revert php-translation/symfony-storage version in composer.json * Revert making php_translation.extractor.php public - use public alias * Revert making php_translation.extractor.twig public - use public alias * Revert making php_translation.extractor public - use public alias * Revert making one more service public in favor of public alias * * Translation Loader or Reader (BC SF3.4) * * StyleCI * * Legacy wrapper for ChildDefiniton/DefinitionDecorator * * StyleCI fix * * One more fix for translation loader or reader * Tweaked service name to translation.loader_or_reader * Bump php-translation/symfony-storage version to ^0.3.4 * Fix styles: Add missing header for new PHP classes * Create LegacyTranslationLoader on compilation to avoid extra checks * Bump symfony-storage version to v0.4.0 * Move test.* services to a separate file and include it for tests * Add a description for LoaderOrReaderPass class * Use new LegacyTranslationReader and LegacyTranslationWriter classes * Make php_translation.importer public and remove its test alias --- Catalogue/CatalogueFetcher.php | 21 ++-- Catalogue/CatalogueWriter.php | 16 +-- .../CompilerPass/LoaderOrReaderPass.php | 38 ++++++++ DependencyInjection/TranslationExtension.php | 29 +++++- Resources/config/services.yml | 11 ++- Resources/config/services_test.yml | 16 +++ Tests/Functional/BundleInitializationTest.php | 6 +- .../Catalogue/CatalogueFetcherTest.php | 97 +++++++++++++++++++ .../Functional/Controller/EditInPlaceTest.php | 4 +- .../CompilerPass/LoaderOrReaderPassTest.php | 35 +++++++ TranslationBundle.php | 2 + composer.json | 26 ++--- phpunit.xml.dist | 4 + 13 files changed, 269 insertions(+), 36 deletions(-) create mode 100644 DependencyInjection/CompilerPass/LoaderOrReaderPass.php create mode 100644 Resources/config/services_test.yml create mode 100644 Tests/Functional/Catalogue/CatalogueFetcherTest.php create mode 100644 Tests/Unit/DependencyInjection/CompilerPass/LoaderOrReaderPassTest.php diff --git a/Catalogue/CatalogueFetcher.php b/Catalogue/CatalogueFetcher.php index 4f5c954e..4b666307 100644 --- a/Catalogue/CatalogueFetcher.php +++ b/Catalogue/CatalogueFetcher.php @@ -11,9 +11,12 @@ namespace Translation\Bundle\Catalogue; -use Symfony\Bundle\FrameworkBundle\Translation\TranslationLoader; +use Symfony\Bundle\FrameworkBundle\Translation\TranslationLoader as SymfonyTranslationLoader; use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\Reader\TranslationReaderInterface; use Translation\Bundle\Model\Configuration; +use Translation\SymfonyStorage\LegacyTranslationReader; +use Translation\SymfonyStorage\TranslationLoader; /** * Fetches catalogues from source files. This will only work with local file storage @@ -26,16 +29,20 @@ final class CatalogueFetcher { /** - * @var TranslationLoader + * @var TranslationReaderInterface */ - private $loader; + private $reader; /** - * @param TranslationLoader $loader + * @param SymfonyTranslationLoader|TranslationLoader|TranslationReaderInterface $reader */ - public function __construct(TranslationLoader $loader) + public function __construct($reader) { - $this->loader = $loader; + if (!$reader instanceof TranslationReaderInterface) { + $reader = new LegacyTranslationReader($reader); + } + + $this->reader = $reader; } /** @@ -57,7 +64,7 @@ public function getCatalogues(Configuration $config, array $locales = []) $currentCatalogue = new MessageCatalogue($locale); foreach ($dirs as $path) { if (is_dir($path)) { - $this->loader->loadMessages($path, $currentCatalogue); + $this->reader->read($path, $currentCatalogue); } } $catalogues[] = $currentCatalogue; diff --git a/Catalogue/CatalogueWriter.php b/Catalogue/CatalogueWriter.php index 59ea0aa1..52bff4c7 100644 --- a/Catalogue/CatalogueWriter.php +++ b/Catalogue/CatalogueWriter.php @@ -13,7 +13,9 @@ use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Writer\TranslationWriter; +use Symfony\Component\Translation\Writer\TranslationWriterInterface; use Translation\Bundle\Model\Configuration; +use Translation\SymfonyStorage\LegacyTranslationWriter; /** * Write catalogues back to disk. @@ -25,7 +27,7 @@ final class CatalogueWriter { /** - * @var TranslationWriter + * @var TranslationWriterInterface */ private $writer; @@ -38,10 +40,12 @@ final class CatalogueWriter * @param TranslationWriter $writer * @param string $defaultLocale */ - public function __construct( - TranslationWriter $writer, - $defaultLocale - ) { + public function __construct(TranslationWriter $writer, $defaultLocale) + { + if (!$writer instanceof TranslationWriterInterface) { + $writer = new LegacyTranslationWriter($writer); + } + $this->writer = $writer; $this->defaultLocale = $defaultLocale; } @@ -53,7 +57,7 @@ public function __construct( public function writeCatalogues(Configuration $config, array $catalogues) { foreach ($catalogues as $catalogue) { - $this->writer->writeTranslations( + $this->writer->write( $catalogue, $config->getOutputFormat(), [ diff --git a/DependencyInjection/CompilerPass/LoaderOrReaderPass.php b/DependencyInjection/CompilerPass/LoaderOrReaderPass.php new file mode 100644 index 00000000..513e13f3 --- /dev/null +++ b/DependencyInjection/CompilerPass/LoaderOrReaderPass.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\DependencyInjection\CompilerPass; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * To provide a BC layer for Symfony 2.7 to 3.3 this compiler pass + * registers an alias of whether TranslationReader or TranslationLoader + * to be able to inject it in other services. + */ +class LoaderOrReaderPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if ($container->has('translation.reader')) { + $container->setAlias('translation.loader_or_reader', 'translation.reader'); + + return; + } + + if ($container->has('translation.loader')) { + $container->setAlias('translation.loader_or_reader', 'translation.loader'); + + return; + } + } +} diff --git a/DependencyInjection/TranslationExtension.php b/DependencyInjection/TranslationExtension.php index 8aaf753f..28d11f7b 100644 --- a/DependencyInjection/TranslationExtension.php +++ b/DependencyInjection/TranslationExtension.php @@ -11,8 +11,10 @@ namespace Translation\Bundle\DependencyInjection; +use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\DefinitionDecorator; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; @@ -74,6 +76,10 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('auto_translation.yml'); $this->enableFallbackAutoTranslator($container, $config); } + + if ('test' === getenv('ENV')) { + $loader->load('services_test.yml'); + } } /** @@ -105,8 +111,9 @@ private function handleConfigNode(ContainerBuilder $container, array $config) /* * Configure storage chain service */ - $storageDefinition = new DefinitionDecorator('php_translation.storage.abstract'); + $storageDefinition = $this->createChildDefinition('php_translation.storage.abstract'); $storageDefinition->replaceArgument(2, new Reference($configurationServiceId)); + $storageDefinition->setPublic(true); $container->setDefinition('php_translation.storage.'.$name, $storageDefinition); // Add storages @@ -121,7 +128,7 @@ private function handleConfigNode(ContainerBuilder $container, array $config) continue; } - $def = new DefinitionDecorator($serviceId); + $def = $this->createChildDefinition($serviceId); $def->replaceArgument(2, [$c['output_dir']]) ->replaceArgument(3, [$c['local_file_storage_options']]) ->addTag('php_translation.storage', ['type' => 'local', 'name' => $name]); @@ -131,7 +138,7 @@ private function handleConfigNode(ContainerBuilder $container, array $config) if (null !== $first) { // Create some aliases for the default storage - $container->setAlias('php_translation.storage', 'php_translation.storage.'.$first); + $container->setAlias('php_translation.storage', new Alias('php_translation.storage.'.$first, true)); if ('default' !== $first) { $container->setAlias('php_translation.storage.default', 'php_translation.storage.'.$first); } @@ -222,4 +229,20 @@ public function getAlias() { return 'translation'; } + + /** + * To avoid BC break for Symfony 3.3+. + * + * @param $parent + * + * @return ChildDefinition|DefinitionDecorator + */ + private function createChildDefinition($parent) + { + if (class_exists('Symfony\Component\DependencyInjection\ChildDefinition')) { + return new ChildDefinition($parent); + } + + return new DefinitionDecorator($parent); + } } diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 7a5d0721..69528e73 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -1,9 +1,11 @@ services: php_translation.catalogue_fetcher: + public: true class: Translation\Bundle\Catalogue\CatalogueFetcher - arguments: ["@translation.loader"] + arguments: ["@translation.loader_or_reader"] php_translation.catalogue_writer: + public: true class: Translation\Bundle\Catalogue\CatalogueWriter arguments: ["@translation.writer", "%php_translation.default_locale%"] @@ -13,26 +15,30 @@ services: arguments: ["@php_translation.catalogue_fetcher", "@php_translation.catalogue_writer", ~] php_translation.catalogue_manager: + public: true class: Translation\Bundle\Catalogue\CatalogueManager php_translation.extractor: class: Translation\Extractor\Extractor php_translation.configuration_manager: + public: true class: Translation\Bundle\Service\ConfigurationManager php_translation.importer: + public: true class: Translation\Bundle\Service\Importer arguments: ["@php_translation.extractor"] php_translation.cache_clearer: + public: true class: Translation\Bundle\Service\CacheClearer arguments: ["%kernel.cache_dir%", "@translator", "@filesystem"] php_translation.local_file_storage.abstract: class: Translation\SymfonyStorage\FileStorage abstract: true - arguments: ["@translation.writer", "@translation.loader", ~, []] + arguments: ["@translation.writer", "@translation.loader_or_reader", ~, []] php_translation.storage.xlf_loader: class: Translation\SymfonyStorage\Loader\XliffLoader @@ -45,5 +51,6 @@ services: - { name: translation.dumper, alias: xlf, legacy-alias: xliff } php_translation.catalogue_counter: + public: true class: Translation\Bundle\Catalogue\CatalogueCounter arguments: [] diff --git a/Resources/config/services_test.yml b/Resources/config/services_test.yml new file mode 100644 index 00000000..6dc80eae --- /dev/null +++ b/Resources/config/services_test.yml @@ -0,0 +1,16 @@ +services: + test.php_translation.edit_in_place.activator: + public: true + alias: 'php_translation.edit_in_place.activator' + + test.php_translation.extractor.php: + public: true + alias: 'php_translation.extractor.php' + + test.php_translation.extractor.twig: + public: true + alias: 'php_translation.extractor.twig' + + test.php_translation.extractor: + public: true + alias: 'php_translation.extractor' diff --git a/Tests/Functional/BundleInitializationTest.php b/Tests/Functional/BundleInitializationTest.php index 62619106..63f7a07e 100644 --- a/Tests/Functional/BundleInitializationTest.php +++ b/Tests/Functional/BundleInitializationTest.php @@ -36,12 +36,12 @@ public function testRegisterBundle() $services = [ 'php_translation.storage' => StorageService::class, - 'php_translation.extractor.twig' => TwigFileExtractor::class, - 'php_translation.extractor.php' => PHPFileExtractor::class, + 'test.php_translation.extractor.twig' => TwigFileExtractor::class, + 'test.php_translation.extractor.php' => PHPFileExtractor::class, 'php_translation.catalogue_fetcher' => CatalogueFetcher::class, 'php_translation.catalogue_writer' => CatalogueWriter::class, 'php_translation.catalogue_manager' => CatalogueManager::class, - 'php_translation.extractor' => Extractor::class, + 'test.php_translation.extractor' => Extractor::class, ]; foreach ($services as $id => $class) { diff --git a/Tests/Functional/Catalogue/CatalogueFetcherTest.php b/Tests/Functional/Catalogue/CatalogueFetcherTest.php new file mode 100644 index 00000000..e7bc5be5 --- /dev/null +++ b/Tests/Functional/Catalogue/CatalogueFetcherTest.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Component\Translation\MessageCatalogue; +use Translation\Bundle\Catalogue\CatalogueFetcher; +use Translation\Bundle\Model\Configuration; +use Translation\Bundle\Tests\Functional\BaseTestCase; + +class CatalogueFetcherTest extends BaseTestCase +{ + /** + * @var CatalogueFetcher + */ + private $catalogueFetcher; + + public static function setUpBeforeClass() + { + parent::setUpBeforeClass(); + + file_put_contents( + __DIR__.'/../app/Resources/translations/messages.sv.xlf', + <<<'XML' + + + + + + key0 + trans0 + + + + + key1 + trans1 + + + + + +XML + ); + } + + public function testFetchCatalogue() + { + $this->bootKernel(); + + $this->catalogueFetcher = $this->getContainer()->get('php_translation.catalogue_fetcher'); + + $data = self::getDefaultData(); + $data['external_translations_dirs'] = [__DIR__.'/../app/Resources/translations/']; + + $conf = new Configuration($data); + + /** @var MessageCatalogue[] $catalogues */ + $catalogues = $this->catalogueFetcher->getCatalogues($conf, ['sv']); + + $this->assertEquals('sv', $catalogues[0]->getLocale()); + } + + /** + * @return array + */ + public static function getDefaultData() + { + return [ + 'name' => 'getName', + 'locales' => ['getLocales'], + 'project_root' => 'getProjectRoot', + 'output_dir' => 'getOutputDir', + 'dirs' => ['getDirs'], + 'excluded_dirs' => ['getExcludedDirs'], + 'excluded_names' => ['getExcludedNames'], + 'external_translations_dirs' => ['getExternalTranslationsDirs'], + 'output_format' => 'getOutputFormat', + 'blacklist_domains' => ['getBlacklistDomains'], + 'whitelist_domains' => ['getWhitelistDomains'], + 'xliff_version' => ['getXliffVersion'], + ]; + } + + protected function setUp() + { + parent::setUp(); + + $this->kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yml'); + } +} diff --git a/Tests/Functional/Controller/EditInPlaceTest.php b/Tests/Functional/Controller/EditInPlaceTest.php index 9a968235..9026b7c6 100644 --- a/Tests/Functional/Controller/EditInPlaceTest.php +++ b/Tests/Functional/Controller/EditInPlaceTest.php @@ -25,7 +25,7 @@ public function testActivatedTest() $request = Request::create('/foobar'); // Activate the feature - $this->getContainer()->get('php_translation.edit_in_place.activator')->activate(); + $this->getContainer()->get('test.php_translation.edit_in_place.activator')->activate(); $response = $this->kernel->handle($request); @@ -57,7 +57,7 @@ public function testIfUntranslatableLabelGetsDisabled() // Activate the feature $this->bootKernel(); - $this->getContainer()->get('php_translation.edit_in_place.activator')->activate(); + $this->getContainer()->get('test.php_translation.edit_in_place.activator')->activate(); $response = $this->kernel->handle($request); diff --git a/Tests/Unit/DependencyInjection/CompilerPass/LoaderOrReaderPassTest.php b/Tests/Unit/DependencyInjection/CompilerPass/LoaderOrReaderPassTest.php new file mode 100644 index 00000000..ea9b66f0 --- /dev/null +++ b/Tests/Unit/DependencyInjection/CompilerPass/LoaderOrReaderPassTest.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Tests\Unit\DependencyInjection\CompilerPass; + +use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractCompilerPassTestCase; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Translation\Bundle\DependencyInjection\CompilerPass\LoaderOrReaderPass; + +class LoaderOrReaderPassTest extends AbstractCompilerPassTestCase +{ + protected function registerCompilerPass(ContainerBuilder $container) + { + $container->addCompilerPass(new LoaderOrReaderPass()); + } + + public function testLoaderOrReader() + { + $def = new Definition(); + $this->setDefinition('translation.reader', $def); + + $this->compile(); + + $this->assertContainerBuilderHasAlias('translation.loader_or_reader'); + } +} diff --git a/TranslationBundle.php b/TranslationBundle.php index 99f997b0..f8976c04 100644 --- a/TranslationBundle.php +++ b/TranslationBundle.php @@ -16,6 +16,7 @@ use Translation\Bundle\DependencyInjection\CompilerPass\EditInPlacePass; use Translation\Bundle\DependencyInjection\CompilerPass\ExternalTranslatorPass; use Translation\Bundle\DependencyInjection\CompilerPass\ExtractorPass; +use Translation\Bundle\DependencyInjection\CompilerPass\LoaderOrReaderPass; use Translation\Bundle\DependencyInjection\CompilerPass\StoragePass; use Translation\Bundle\DependencyInjection\CompilerPass\SymfonyProfilerPass; use Translation\Bundle\DependencyInjection\CompilerPass\ValidatorVisitorPass; @@ -33,5 +34,6 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new ExtractorPass()); $container->addCompilerPass(new StoragePass()); $container->addCompilerPass(new EditInPlacePass()); + $container->addCompilerPass(new LoaderOrReaderPass()); } } diff --git a/composer.json b/composer.json index 18130fe9..56062828 100644 --- a/composer.json +++ b/composer.json @@ -11,14 +11,14 @@ ], "require": { "php": "^5.5 || ^7.0", - "symfony/framework-bundle": "^2.7 || ^3.0", - "symfony/validator": "^2.7 || ^3.0", - "symfony/translation": "^2.7 || ^3.0", - "symfony/finder": "^2.7 || ^3.0", - "symfony/intl": "^2.7 || ^3.0", + "symfony/framework-bundle": "^2.7 || ^3.0 || ^4.0", + "symfony/validator": "^2.7 || ^3.0 || ^4.0", + "symfony/translation": "^2.7 || ^3.0 || ^4.0", + "symfony/finder": "^2.7 || ^3.0 || ^4.0", + "symfony/intl": "^2.7 || ^3.0 || ^4.0", "php-translation/common": "^0.2.1", - "php-translation/symfony-storage": "^0.3.2", + "php-translation/symfony-storage": "^0.4.0", "php-translation/extractor": "^1.2" }, "require-dev": { @@ -27,13 +27,13 @@ "php-http/curl-client": "^1.7", "php-http/message": "^1.6", "php-http/message-factory": "^1.0.2", - "symfony/console": "^2.7 || ^3.0", - "symfony/twig-bundle": "^2.7 || ^3.0", - "symfony/twig-bridge": "^2.7 || ^3.0", - "symfony/asset": "^2.7 || ^3.0", - "symfony/templating": "^2.7 || ^3.0", - "symfony/dependency-injection": "^2.7 || ^3.0", - "symfony/web-profiler-bundle": "^2.7 || ^3.0", + "symfony/console": "^2.7 || ^3.0 || ^4.0", + "symfony/twig-bundle": "^2.7 || ^3.0 || ^4.0", + "symfony/twig-bridge": "^2.7 || ^3.0 || ^4.0", + "symfony/asset": "^2.7 || ^3.0 || ^4.0", + "symfony/templating": "^2.7 || ^3.0 || ^4.0", + "symfony/dependency-injection": "^2.7 || ^3.0 || ^4.0", + "symfony/web-profiler-bundle": "^2.7 || ^3.0 || ^4.0", "matthiasnoback/symfony-dependency-injection-test": "^1.0 || ^2.0", "guzzlehttp/psr7": "^1.4", "nyholm/nsa": "^1.1", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9ab4f64a..5a93c70c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -13,6 +13,10 @@ bootstrap="./vendor/autoload.php" > + + + + From 3053d61a49701b199843edb9d06775a55b38590e Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 28 Dec 2017 20:26:10 +0100 Subject: [PATCH 010/234] Improved travis config to test on sf4 (#161) * Added travis config * Bugfix * minor * Temporary allow deprecations * Added property access in sf 2 * Better versions of matthias nobacks tests * Using phpunit 5.7 --- .travis.yml | 73 ++++++++++++++++++++++++++++++++++----------------- composer.json | 13 +++++---- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/.travis.yml b/.travis.yml index f2aefc53..f75590e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,43 +9,68 @@ branches: - /^analysis-.*$/ - /^patch-.*$/ -php: - - 5.5 - - 5.6 - - 7.0 - - 7.1 - - 7.2 - env: global: - TEST_COMMAND="composer test" + - SYMFONY_PHPUNIT_VERSION="6.3" + - SYMFONY_DEPRECATIONS_HELPER="weak" # Temporary, To be removed matrix: - fast_finish: true - include: - - php: 5.5 - env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" COVERAGE=true TEST_COMMAND="composer test-ci" - - php: 7.1 - env: DEPENDENCIES=dev - # Test against LTS versions - - php: 7.0 - env: SYMFONY_VERSION=2.7.* - - php: 7.0 - env: SYMFONY_VERSION=2.8.* PACKAGES=twig/twig:^1.34 - - php: 7.0 - env: SYMFONY_VERSION=2.8.* + fast_finish: true + include: + # Test with lowest dependencies + - php: 7.1 + env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" SYMFONY_DEPRECATIONS_HELPER="weak" SYMFONY_PHPUNIT_VERSION="5.7" + - php: 5.5 + env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" SYMFONY_DEPRECATIONS_HELPER="weak" SYMFONY_PHPUNIT_VERSION="5.7" + + # Test the latest stable release + - php: 5.5 + env: SYMFONY_PHPUNIT_VERSION="5.7" + - php: 5.6 + env: SYMFONY_PHPUNIT_VERSION="5.7" + - php: 7.0 + - php: 7.1 + - php: 7.2 + env: COVERAGE=true TEST_COMMAND="composer test-ci" + + # Force some major versions of Symfony + - php: 7.2 + env: DEPENDENCIES="dunglas/symfony-lock:^2 symfony/property-access:^2.8" + - php: 7.2 + env: DEPENDENCIES="dunglas/symfony-lock:^3" + - php: 7.2 + env: DEPENDENCIES="dunglas/symfony-lock:^4" + - php: 7.0 + env: DEPENDENCIES="dunglas/symfony-lock:^2 twig/twig:^1.34 symfony/property-access:^2.8" + + # Latest commit to master + - php: 7.2 + env: STABILITY="dev" + + allow_failures: + # Dev-master is allowed to fail. + - env: STABILITY="dev" before_install: - - if [ "$SYMFONY_VERSION" != "" ]; then composer require "symfony/symfony:${SYMFONY_VERSION}" --no-update; fi; - - if [ "$PACKAGES" != "" ]; then composer require $PACKAGES --no-update; fi; - - if [ "$DEPENDENCIES" = "dev" ]; then perl -pi -e 's/^}$/,"minimum-stability":"dev"}/' composer.json; fi; + - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi + - if ! [ -z "$STABILITY" ]; then composer config minimum-stability ${STABILITY}; fi; + - if ! [ -z "$DEPENDENCIES" ]; then composer require --no-update ${DEPENDENCIES}; fi; install: - - travis_retry composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction + # To be removed when this issue will be resolved: https://github.com/composer/composer/issues/5355 + - if [[ "$COMPOSER_FLAGS" == *"--prefer-lowest"* ]]; then composer update --prefer-dist --no-interaction --prefer-stable --quiet; fi + - composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction + - vendor/bin/simple-phpunit install script: + - composer validate --strict --no-check-lock - $TEST_COMMAND after_success: - if [[ $COVERAGE = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi - if [[ $COVERAGE = true ]]; then php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml; fi + +after_script: + - wget http://tnyholm.se/reporter.phar + - php reporter.phar build:upload diff --git a/composer.json b/composer.json index 56062828..054282f1 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "php-translation/extractor": "^1.2" }, "require-dev": { - "phpunit/phpunit": "^4.8.36 || ^5.5 || ^6.2", + "symfony/phpunit-bridge": "^3.4 || ^4.0", "php-translation/translator": "^0.1", "php-http/curl-client": "^1.7", "php-http/message": "^1.6", @@ -34,10 +34,11 @@ "symfony/templating": "^2.7 || ^3.0 || ^4.0", "symfony/dependency-injection": "^2.7 || ^3.0 || ^4.0", "symfony/web-profiler-bundle": "^2.7 || ^3.0 || ^4.0", - "matthiasnoback/symfony-dependency-injection-test": "^1.0 || ^2.0", + "matthiasnoback/symfony-dependency-injection-test": "^1.2 || ^2.3", + "matthiasnoback/symfony-config-test": "^2.2 || ^3.1", "guzzlehttp/psr7": "^1.4", "nyholm/nsa": "^1.1", - "nyholm/symfony-bundle-test": "^1.2" + "nyholm/symfony-bundle-test": "^1.2.3" }, "suggest": { "php-http/httplug-bundle": "To easier configure your httplug clients." @@ -46,11 +47,9 @@ "psr-4": { "Translation\\Bundle\\": "" } }, "scripts": { - "test": "vendor/bin/phpunit", - "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml" + "test": "vendor/bin/simple-phpunit", + "test-ci": "vendor/bin/simple-phpunit --coverage-text --coverage-clover=build/coverage.xml" }, - "minimum-stability": "dev", - "prefer-stable": true, "extra": { "branch-alias": { "dev-master": "0.4-dev" From b3cce41c05fea351d0d0d5f2bde04e5dc19d9bc9 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 28 Dec 2017 21:00:47 +0100 Subject: [PATCH 011/234] Prepare for 0.5.0 (#162) --- Changelog.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index 195d0b28..8a4a8e39 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,11 +2,18 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. -## Unreleased +## 0.5.0 -## Added +### Added +- Symfony 4 support - New `--cache` option on the `translation:download` allowing to clear the cache automatically if the downloaded translations have changed. +- Support for Yandex translator + +### Fixed + +- Wrong paths in web profiler when using Twig2.x. +- Some JavaScript errors. ## 0.4.0 From fca28b4e3842057402a857125a1622c13863a21d Mon Sep 17 00:00:00 2001 From: Miha Vrhovnik Date: Fri, 29 Dec 2017 11:26:48 +0100 Subject: [PATCH 012/234] Register the commands as services. --- DependencyInjection/TranslationExtension.php | 2 ++ Resources/config/console.yml | 26 ++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 Resources/config/console.yml diff --git a/DependencyInjection/TranslationExtension.php b/DependencyInjection/TranslationExtension.php index 28d11f7b..608c17a7 100644 --- a/DependencyInjection/TranslationExtension.php +++ b/DependencyInjection/TranslationExtension.php @@ -80,6 +80,8 @@ public function load(array $configs, ContainerBuilder $container) if ('test' === getenv('ENV')) { $loader->load('services_test.yml'); } + + $loader->load('console.yml'); } /** diff --git a/Resources/config/console.yml b/Resources/config/console.yml new file mode 100644 index 00000000..60f25741 --- /dev/null +++ b/Resources/config/console.yml @@ -0,0 +1,26 @@ +services: + php_translator.console.delete_obsolete: + class: Translation\Bundle\Command\DeleteObsoleteCommand + public: true + tags: + - { name: console.command, command: translation:delete-obsolete} + php_translator.console.download: + class: Translation\Bundle\Command\DownloadCommand + public: true + tags: + - { name: console.command, command: translation:download } + php_translator.console.extract: + class: Translation\Bundle\Command\ExtractCommand + public: true + tags: + - { name: console.command, command: translation:extract } + php_translator.console.status: + class: Translation\Bundle\Command\StatusCommand + public: true + tags: + - { name: console.command, command: translation:status } + php_translator.console.sync: + class: Translation\Bundle\Command\SyncCommand + public: true + tags: + - { name: console.command, command: translation:sync } From 0709136f58d5ba945560da79ea7be4d568b40792 Mon Sep 17 00:00:00 2001 From: Miha Vrhovnik Date: Sat, 30 Dec 2017 14:39:29 +0100 Subject: [PATCH 013/234] Register twig desc filter (#165) --- Resources/config/services.yml | 5 +++++ composer.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 69528e73..89bd5b9e 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -54,3 +54,8 @@ services: public: true class: Translation\Bundle\Catalogue\CatalogueCounter arguments: [] + + php_translation.twig_extension: + class: Translation\Extractor\Twig\TranslationExtension + tags: + - { name: twig.extension } \ No newline at end of file diff --git a/composer.json b/composer.json index 054282f1..25d39b2d 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "php-translation/common": "^0.2.1", "php-translation/symfony-storage": "^0.4.0", - "php-translation/extractor": "^1.2" + "php-translation/extractor": "^1.3" }, "require-dev": { "symfony/phpunit-bridge": "^3.4 || ^4.0", From 396a4419430d82a3853fda297a499454852c7527 Mon Sep 17 00:00:00 2001 From: Miha Vrhovnik Date: Sun, 31 Dec 2017 13:20:06 +0100 Subject: [PATCH 014/234] Extract/update only for one bundle (#166) --- Command/BundleTrait.php | 38 +++++++++++++++++++++++++++++++ Command/DeleteObsoleteCommand.php | 8 ++++++- Command/DownloadCommand.php | 4 ++++ Command/ExtractCommand.php | 7 +++++- Command/StatusCommand.php | 7 +++++- Model/Configuration.php | 13 +++++++++++ 6 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 Command/BundleTrait.php diff --git a/Command/BundleTrait.php b/Command/BundleTrait.php new file mode 100644 index 00000000..e555a8bc --- /dev/null +++ b/Command/BundleTrait.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Command; + +use Symfony\Component\Console\Input\InputInterface; +use Translation\Bundle\Model\Configuration; + +trait BundleTrait +{ + private function configureBundleDirs(InputInterface $input, Configuration $config) + { + if ($bundleName = $input->getOption('bundle')) { + if ('@' === $bundleName[0]) { + if (false === $pos = strpos($bundleName, '/')) { + $bundleName = substr($bundleName, 1); + } else { + $bundleName = substr($bundleName, 1, $pos - 2); + } + } + + $bundle = $this->getApplication() + ->getKernel() + ->getBundle($bundleName) + ; + + $config->reconfigureBundleDirs($bundle->getPath(), $bundle->getPath().'/Resources/translations'); + } + } +} diff --git a/Command/DeleteObsoleteCommand.php b/Command/DeleteObsoleteCommand.php index b0643406..8ded1ee9 100644 --- a/Command/DeleteObsoleteCommand.php +++ b/Command/DeleteObsoleteCommand.php @@ -15,6 +15,7 @@ use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; @@ -23,13 +24,17 @@ */ class DeleteObsoleteCommand extends ContainerAwareCommand { + use BundleTrait; + protected function configure() { $this ->setName('translation:delete-obsolete') ->setDescription('Delete all translations marked as obsolete.') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') - ->addArgument('locale', InputArgument::OPTIONAL, 'The locale ot use. If omitted, we use all configured locales.', null); + ->addArgument('locale', InputArgument::OPTIONAL, 'The locale ot use. If omitted, we use all configured locales.', null) + ->addOption('bundle', 'b', InputOption::VALUE_REQUIRED, 'The bundle you want remove translations from.') + ; } protected function execute(InputInterface $input, OutputInterface $output) @@ -43,6 +48,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $catalogueManager = $container->get('php_translation.catalogue_manager'); $config = $container->get('php_translation.configuration_manager')->getConfiguration($configName); + $this->configureBundleDirs($input, $config); $catalogueManager->load($container->get('php_translation.catalogue_fetcher')->getCatalogues($config, $locales)); $storage = $container->get('php_translation.storage.'.$configName); diff --git a/Command/DownloadCommand.php b/Command/DownloadCommand.php index 881df632..53bfa0d8 100644 --- a/Command/DownloadCommand.php +++ b/Command/DownloadCommand.php @@ -25,6 +25,8 @@ */ class DownloadCommand extends ContainerAwareCommand { + use BundleTrait; + protected function configure() { $this @@ -32,6 +34,7 @@ protected function configure() ->setDescription('Replace local messages with messages from remote') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') ->addOption('cache', null, InputOption::VALUE_NONE, 'Clear the cache if the translations have changed') + ->addOption('bundle', 'b', InputOption::VALUE_REQUIRED, 'The bundle you want update translations from.') ; } @@ -44,6 +47,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $storage = $container->get('php_translation.storage.'.$configName); /** @var Configuration $configuration */ $configuration = $this->getContainer()->get('php_translation.configuration.'.$configName); + $this->configureBundleDirs($input, $configuration); if ($input->getOption('cache')) { $translationsDirectory = $configuration->getOutputDir(); diff --git a/Command/ExtractCommand.php b/Command/ExtractCommand.php index 50a0d3ff..4a81aad5 100644 --- a/Command/ExtractCommand.php +++ b/Command/ExtractCommand.php @@ -26,6 +26,8 @@ */ class ExtractCommand extends ContainerAwareCommand { + use BundleTrait; + protected function configure() { $this @@ -33,7 +35,9 @@ protected function configure() ->setDescription('Extract translations from source code.') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') ->addArgument('locale', InputArgument::OPTIONAL, 'The locale ot use. If omitted, we use all configured locales.', false) - ->addOption('hide-errors', null, InputOption::VALUE_NONE, 'If we should print error or not'); + ->addOption('hide-errors', null, InputOption::VALUE_NONE, 'If we should print error or not') + ->addOption('bundle', 'b', InputOption::VALUE_REQUIRED, 'The bundle you want extract translations from.') + ; } protected function execute(InputInterface $input, OutputInterface $output) @@ -51,6 +55,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $catalogues = $container->get('php_translation.catalogue_fetcher') ->getCatalogues($config, $locales); + $this->configureBundleDirs($input, $config); $finder = $this->getConfiguredFinder($config); $result = $importer->extractToCatalogues($finder, $catalogues, [ diff --git a/Command/StatusCommand.php b/Command/StatusCommand.php index 76652e4e..17682d78 100644 --- a/Command/StatusCommand.php +++ b/Command/StatusCommand.php @@ -23,6 +23,8 @@ */ class StatusCommand extends ContainerAwareCommand { + use BundleTrait; + protected function configure() { $this @@ -30,7 +32,9 @@ protected function configure() ->setDescription('Show status about your translations.') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') ->addArgument('locale', InputArgument::OPTIONAL, 'The locale ot use. If omitted, we use all configured locales.', false) - ->addOption('json', null, InputOption::VALUE_NONE, 'If we should output in Json format'); + ->addOption('json', null, InputOption::VALUE_NONE, 'If we should output in Json format') + ->addOption('bundle', 'b', InputOption::VALUE_REQUIRED, 'The translations for bundle you want to check.') + ; } protected function execute(InputInterface $input, OutputInterface $output) @@ -39,6 +43,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $counter = $container->get('php_translation.catalogue_counter'); $config = $container->get('php_translation.configuration_manager') ->getConfiguration($input->getArgument('configuration')); + $this->configureBundleDirs($input, $config); $locales = []; if ($inputLocale = $input->getArgument('locale')) { diff --git a/Model/Configuration.php b/Model/Configuration.php index 512aaa34..763e5d15 100644 --- a/Model/Configuration.php +++ b/Model/Configuration.php @@ -217,4 +217,17 @@ public function getXliffVersion() { return $this->xliffVersion; } + + /** + * Reconfigures the directories so we can use one configuration for extracting + * the messages only from one bundle. + * + * @param string $bundleDir + * @param string $outputDir + */ + public function reconfigureBundleDirs($bundleDir, $outputDir) + { + $this->dirs = is_array($bundleDir) ? $bundleDir : [$bundleDir]; + $this->outputDir = $outputDir; + } } From 7d840ca000fd1029355bab5c56d4e384e90cb703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20=C3=85rhof?= Date: Sun, 31 Dec 2017 13:20:50 +0100 Subject: [PATCH 015/234] Removing empty messages in the total count in webui, gives a better progress (#170) * Removing empty in the total count, so it gives a better progress in the webui * Style fix --- Controller/WebUIController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index 89d53a31..dc1f9c2a 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -58,7 +58,9 @@ public function indexAction($configName = null) ksort($domains); $catalogueSize[$locale] = 0; foreach ($domains as $domain => $messages) { - $count = count($messages); + $count = count(array_filter($messages, function ($message) { + return '' !== $message; + })); $catalogueSize[$locale] += $count; if (!isset($maxDomainSize[$domain]) || $count > $maxDomainSize[$domain]) { $maxDomainSize[$domain] = $count; From 8884fed838720b5cd34dd39313ad29bc5d627c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20=C3=85rhof?= Date: Sun, 31 Dec 2017 13:47:00 +0100 Subject: [PATCH 016/234] Division by zero and corrected the progress calculation (#171) --- Resources/views/WebUI/index.html.twig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Resources/views/WebUI/index.html.twig b/Resources/views/WebUI/index.html.twig index 55d5f0e9..c209975b 100644 --- a/Resources/views/WebUI/index.html.twig +++ b/Resources/views/WebUI/index.html.twig @@ -9,7 +9,10 @@

{{ localeMap[cataloge.locale] }}

{% for domain,messages in cataloge.all %} - {% set pg = (100*messages|length/maxDomainSize[domain])|round %} + {% set pg = maxDomainSize[domain] %} + {% if pg > 0 %} + {% set pg = (pg/messages|length)|round(2)*100 %} + {% endif %} From d08624c622a09e2eca1ab93828455ee4c728566d Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 12 Feb 2018 11:34:46 +0100 Subject: [PATCH 030/234] Inject depedencies to the commands (#194) * Inject depedencies to the commands * cs and test fix * cs * Access storage from storage service * Support for sf3.2 * Make "php_translation.storage_manager" public * Fixed typo --- Command/BundleTrait.php | 2 + Command/DeleteObsoleteCommand.php | 61 +++++++++++--- Command/DownloadCommand.php | 55 ++++++++++--- Command/ExtractCommand.php | 80 +++++++++++++++---- Command/StatusCommand.php | 53 +++++++++--- Command/SyncCommand.php | 30 +++++-- Controller/EditInPlaceController.php | 2 +- Controller/WebUIController.php | 6 +- DependencyInjection/TranslationExtension.php | 2 + Resources/config/console.yml | 25 ++++++ Resources/config/services.yml | 4 + Service/StorageManager.php | 75 +++++++++++++++++ .../Functional/Command/ExtractCommandTest.php | 4 +- .../Functional/Command/StatusCommandTest.php | 4 +- 14 files changed, 339 insertions(+), 64 deletions(-) create mode 100644 Service/StorageManager.php diff --git a/Command/BundleTrait.php b/Command/BundleTrait.php index e555a8bc..37397d1e 100644 --- a/Command/BundleTrait.php +++ b/Command/BundleTrait.php @@ -12,6 +12,7 @@ namespace Translation\Bundle\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\HttpKernel\Bundle\Bundle; use Translation\Bundle\Model\Configuration; trait BundleTrait @@ -27,6 +28,7 @@ private function configureBundleDirs(InputInterface $input, Configuration $confi } } + /** @var Bundle $bundle */ $bundle = $this->getApplication() ->getKernel() ->getBundle($bundleName) diff --git a/Command/DeleteObsoleteCommand.php b/Command/DeleteObsoleteCommand.php index 215cb62e..911af231 100644 --- a/Command/DeleteObsoleteCommand.php +++ b/Command/DeleteObsoleteCommand.php @@ -11,25 +11,70 @@ namespace Translation\Bundle\Command; -use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputArgument; 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 Translation\Bundle\Catalogue\CatalogueFetcher; +use Translation\Bundle\Catalogue\CatalogueManager; +use Translation\Bundle\Service\ConfigurationManager; +use Translation\Bundle\Service\StorageManager; /** * @author Tobias Nyholm */ -class DeleteObsoleteCommand extends ContainerAwareCommand +class DeleteObsoleteCommand extends Command { use BundleTrait; + protected static $defaultName = 'translation:delete-obsolete'; + + /** + * @var StorageManager + */ + private $storageManager; + + /** + * @var ConfigurationManager + */ + private $configurationManager; + + /** + * @var CatalogueManager + */ + private $catalogueManager; + + /** + * @var CatalogueFetcher + */ + private $catalogueFetcher; + + /** + * @param StorageManager $storageManager + * @param ConfigurationManager $configurationManager + * @param CatalogueManager $catalogueManager + * @param CatalogueFetcher $catalogueFetcher + */ + public function __construct( + StorageManager $storageManager, + ConfigurationManager $configurationManager, + CatalogueManager $catalogueManager, + CatalogueFetcher $catalogueFetcher + ) { + $this->storageManager = $storageManager; + $this->configurationManager = $configurationManager; + $this->catalogueManager = $catalogueManager; + $this->catalogueFetcher = $catalogueFetcher; + parent::__construct(); + } + protected function configure() { $this - ->setName('translation:delete-obsolete') + ->setName(self::$defaultName) ->setDescription('Delete all translations marked as obsolete.') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') ->addArgument('locale', InputArgument::OPTIONAL, 'The locale ot use. If omitted, we use all configured locales.', null) @@ -39,20 +84,18 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { - $container = $this->getContainer(); $configName = $input->getArgument('configuration'); $locales = []; if (null !== $inputLocale = $input->getArgument('locale')) { $locales = [$inputLocale]; } - $catalogueManager = $container->get('php_translation.catalogue_manager'); - $config = $container->get('php_translation.configuration_manager')->getConfiguration($configName); + $config = $this->configurationManager->getConfiguration($configName); $this->configureBundleDirs($input, $config); - $catalogueManager->load($container->get('php_translation.catalogue_fetcher')->getCatalogues($config, $locales)); + $this->catalogueManager->load($this->catalogueFetcher->getCatalogues($config, $locales)); - $storage = $container->get('php_translation.storage.'.$configName); - $messages = $catalogueManager->findMessages(['locale' => $inputLocale, 'isObsolete' => true]); + $storage = $this->storageManager->getStorage($configName); + $messages = $this->catalogueManager->findMessages(['locale' => $inputLocale, 'isObsolete' => true]); $messageCount = count($messages); if (0 === $messageCount) { diff --git a/Command/DownloadCommand.php b/Command/DownloadCommand.php index 3e3ced85..1b201c8c 100644 --- a/Command/DownloadCommand.php +++ b/Command/DownloadCommand.php @@ -11,26 +11,61 @@ namespace Translation\Bundle\Command; -use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Finder\Finder; -use Translation\Bundle\Service\StorageService; +use Translation\Bundle\Service\CacheClearer; +use Translation\Bundle\Service\ConfigurationManager; +use Translation\Bundle\Service\StorageManager; use Translation\Bundle\Model\Configuration; /** * @author Tobias Nyholm */ -class DownloadCommand extends ContainerAwareCommand +class DownloadCommand extends Command { use BundleTrait; + protected static $defaultName = 'translation:download'; + + /** + * @var StorageManager + */ + private $storageManager; + + /** + * @var ConfigurationManager + */ + private $configurationManager; + + /** + * @var CacheClearer + */ + private $cacheCleaner; + + /** + * @param StorageManager $storageManager + * @param ConfigurationManager $configurationManager + * @param CacheClearer $cacheCleaner + */ + public function __construct( + StorageManager $storageManager, + ConfigurationManager $configurationManager, + CacheClearer $cacheCleaner + ) { + $this->storageManager = $storageManager; + $this->configurationManager = $configurationManager; + $this->cacheCleaner = $cacheCleaner; + parent::__construct(); + } + protected function configure() { $this - ->setName('translation:download') + ->setName(self::$defaultName) ->setDescription('Replace local messages with messages from remote') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') ->addOption('cache', null, InputOption::VALUE_NONE, 'Clear the cache if the translations have changed') @@ -40,15 +75,10 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { - $container = $this->getContainer(); $configName = $input->getArgument('configuration'); + $storage = $this->storageManager->getStorage($configName); + $configuration = $this->configurationManager->getConfiguration($configName); - /** @var StorageService $storage */ - $storage = $container->get('php_translation.storage.'.$configName); - - /** @var Configuration $configuration */ - $configuration = $container->get('php_translation.configuration_manager') - ->getConfiguration($input->getArgument('configuration')); $this->configureBundleDirs($input, $configuration); if ($input->getOption('cache')) { @@ -58,8 +88,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $md5AfterDownload = $this->hashDirectory($translationsDirectory); if ($md5BeforeDownload !== $md5AfterDownload) { - $cacheClearer = $this->getContainer()->get('php_translation.cache_clearer'); - $cacheClearer->clearAndWarmUp(); + $this->cacheCleaner->clearAndWarmUp(); } } else { $storage->download(); diff --git a/Command/ExtractCommand.php b/Command/ExtractCommand.php index 4a81aad5..8da04b4d 100644 --- a/Command/ExtractCommand.php +++ b/Command/ExtractCommand.php @@ -11,27 +11,82 @@ namespace Translation\Bundle\Command; -use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Finder\Finder; +use Translation\Bundle\Catalogue\CatalogueCounter; +use Translation\Bundle\Catalogue\CatalogueFetcher; +use Translation\Bundle\Catalogue\CatalogueWriter; use Translation\Bundle\Model\Configuration; +use Translation\Bundle\Service\ConfigurationManager; +use Translation\Bundle\Service\Importer; use Translation\Extractor\Model\Error; /** * @author Tobias Nyholm */ -class ExtractCommand extends ContainerAwareCommand +class ExtractCommand extends Command { use BundleTrait; + protected static $defaultName = 'translation:extract'; + + /** + * @var CatalogueFetcher + */ + private $catalogueFetcher; + + /** + * @var CatalogueWriter + */ + private $catalogueWriter; + + /** + * @var CatalogueCounter + */ + private $catalogueCounter; + + /** + * @var Importer + */ + private $importer; + + /** + * @var ConfigurationManager + */ + private $configurationManager; + + /** + * @param CatalogueFetcher $catalogueFetcher + * @param CatalogueWriter $catalogueWriter + * @param CatalogueCounter $catalogueCounter + * @param Importer $importer + * @param ConfigurationManager $configurationManager + */ + public function __construct( + CatalogueFetcher $catalogueFetcher, + CatalogueWriter $catalogueWriter, + CatalogueCounter $catalogueCounter, + Importer $importer, + ConfigurationManager $configurationManager + ) { + $this->catalogueFetcher = $catalogueFetcher; + $this->catalogueWriter = $catalogueWriter; + $this->catalogueCounter = $catalogueCounter; + $this->importer = $importer; + $this->configurationManager = $configurationManager; + + parent::__construct(); + } + protected function configure() { $this - ->setName('translation:extract') + ->setName(self::$defaultName) ->setDescription('Extract translations from source code.') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') ->addArgument('locale', InputArgument::OPTIONAL, 'The locale ot use. If omitted, we use all configured locales.', false) @@ -42,35 +97,28 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { - $container = $this->getContainer(); - $importer = $container->get('php_translation.importer'); - $config = $container->get('php_translation.configuration_manager') - ->getConfiguration($input->getArgument('configuration')); + $config = $this->configurationManager->getConfiguration($input->getArgument('configuration')); $locales = []; if ($inputLocale = $input->getArgument('locale')) { $locales = [$inputLocale]; } - $catalogues = $container->get('php_translation.catalogue_fetcher') - ->getCatalogues($config, $locales); - + $catalogues = $this->catalogueFetcher->getCatalogues($config, $locales); $this->configureBundleDirs($input, $config); $finder = $this->getConfiguredFinder($config); - $result = $importer->extractToCatalogues($finder, $catalogues, [ + $result = $this->importer->extractToCatalogues($finder, $catalogues, [ 'blacklist_domains' => $config->getBlacklistDomains(), 'whitelist_domains' => $config->getWhitelistDomains(), 'project_root' => $config->getProjectRoot(), ]); $errors = $result->getErrors(); - $container->get('php_translation.catalogue_writer') - ->writeCatalogues($config, $result->getMessageCatalogues()); + $this->catalogueWriter->writeCatalogues($config, $result->getMessageCatalogues()); - $catalogueCounter = $container->get('php_translation.catalogue_counter'); - $definedBefore = $catalogueCounter->getNumberOfDefinedMessages($catalogues[0]); - $definedAfter = $catalogueCounter->getNumberOfDefinedMessages($result->getMessageCatalogues()[0]); + $definedBefore = $this->catalogueCounter->getNumberOfDefinedMessages($catalogues[0]); + $definedAfter = $this->catalogueCounter->getNumberOfDefinedMessages($result->getMessageCatalogues()[0]); /* * Print results diff --git a/Command/StatusCommand.php b/Command/StatusCommand.php index 17682d78..a4617121 100644 --- a/Command/StatusCommand.php +++ b/Command/StatusCommand.php @@ -11,24 +11,61 @@ namespace Translation\Bundle\Command; -use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Translation\Bundle\Catalogue\CatalogueCounter; +use Translation\Bundle\Catalogue\CatalogueFetcher; +use Translation\Bundle\Service\ConfigurationManager; /** * @author Tobias Nyholm */ -class StatusCommand extends ContainerAwareCommand +class StatusCommand extends Command { use BundleTrait; + protected static $defaultName = 'translation:status'; + + /** + * @var CatalogueCounter + */ + private $catalogueCounter; + + /** + * @var ConfigurationManager + */ + private $configurationManager; + + /** + * @var CatalogueFetcher + */ + private $catalogueFetcher; + + /** + * @param CatalogueCounter $catalogueCounter + * @param ConfigurationManager $configurationManager + * @param CatalogueFetcher $catalogueFetcher + */ + public function __construct( + CatalogueCounter $catalogueCounter, + ConfigurationManager $configurationManager, + CatalogueFetcher $catalogueFetcher + ) { + $this->catalogueCounter = $catalogueCounter; + $this->configurationManager = $configurationManager; + $this->catalogueFetcher = $catalogueFetcher; + + parent::__construct(); + } + protected function configure() { $this - ->setName('translation:status') + ->setName(self::$defaultName) ->setDescription('Show status about your translations.') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') ->addArgument('locale', InputArgument::OPTIONAL, 'The locale ot use. If omitted, we use all configured locales.', false) @@ -39,10 +76,7 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { - $container = $this->getContainer(); - $counter = $container->get('php_translation.catalogue_counter'); - $config = $container->get('php_translation.configuration_manager') - ->getConfiguration($input->getArgument('configuration')); + $config = $this->configurationManager->getConfiguration($input->getArgument('configuration')); $this->configureBundleDirs($input, $config); $locales = []; @@ -50,12 +84,11 @@ protected function execute(InputInterface $input, OutputInterface $output) $locales = [$inputLocale]; } - $catalogues = $container->get('php_translation.catalogue_fetcher') - ->getCatalogues($config, $locales); + $catalogues = $this->catalogueFetcher->getCatalogues($config, $locales); $stats = []; foreach ($catalogues as $catalogue) { - $stats[$catalogue->getLocale()] = $counter->getCatalogueStatistics($catalogue); + $stats[$catalogue->getLocale()] = $this->catalogueCounter->getCatalogueStatistics($catalogue); } if ($input->getOption('json')) { diff --git a/Command/SyncCommand.php b/Command/SyncCommand.php index badfc958..dccaf929 100644 --- a/Command/SyncCommand.php +++ b/Command/SyncCommand.php @@ -11,31 +11,45 @@ namespace Translation\Bundle\Command; -use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Translation\Bundle\Service\StorageService; +use Translation\Bundle\Service\StorageManager; /** * @author Tobias Nyholm */ -class SyncCommand extends ContainerAwareCommand +class SyncCommand extends Command { + protected static $defaultName = 'translation:sync'; + + /** + * @var StorageManager + */ + private $storageManager; + + /** + * @param StorageManager $storageManager + */ + public function __construct(StorageManager $storageManager) + { + $this->storageManager = $storageManager; + + parent::__construct(); + } + protected function configure() { $this - ->setName('translation:sync') + ->setName(self::$defaultName) ->setDescription('Sync the translations with the remote storage') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default'); } protected function execute(InputInterface $input, OutputInterface $output) { - $container = $this->getContainer(); $configName = $input->getArgument('configuration'); - /** @var StorageService $storage */ - $storage = $container->get('php_translation.storage.'.$configName); - $storage->sync(); + $this->storageManager->getStorage($configName)->sync(); } } diff --git a/Controller/EditInPlaceController.php b/Controller/EditInPlaceController.php index f737786f..f3f7395f 100644 --- a/Controller/EditInPlaceController.php +++ b/Controller/EditInPlaceController.php @@ -39,7 +39,7 @@ public function editAction(Request $request, $configName, $locale) } /** @var StorageService $storage */ - $storage = $this->get('php_translation.storage.'.$configName); + $storage = $this->get('php_translation.storage_manager')->getStorage($configName); foreach ($messages as $message) { $storage->update($message); } diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index ed1a11ab..f397b540 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -137,7 +137,7 @@ public function createAction(Request $request, $configName, $locale, $domain) } /** @var StorageService $storage */ - $storage = $this->get('php_translation.storage.'.$configName); + $storage = $this->get('php_translation.storage_manager')->getStorage($configName); try { $message = $this->getMessageFromRequest($request); @@ -188,7 +188,7 @@ public function editAction(Request $request, $configName, $locale, $domain) } /** @var StorageService $storage */ - $storage = $this->get('php_translation.storage.'.$configName); + $storage = $this->get('php_translation.storage_manager')->getStorage($configName); $storage->update($message); return new Response('Translation updated'); @@ -218,7 +218,7 @@ public function deleteAction(Request $request, $configName, $locale, $domain) } /** @var StorageService $storage */ - $storage = $this->get('php_translation.storage.'.$configName); + $storage = $this->get('php_translation.storage_manager')->getStorage($configName); $storage->delete($locale, $domain, $message->getKey()); return new Response('Message was deleted'); diff --git a/DependencyInjection/TranslationExtension.php b/DependencyInjection/TranslationExtension.php index 4f9f0ac6..b7848507 100644 --- a/DependencyInjection/TranslationExtension.php +++ b/DependencyInjection/TranslationExtension.php @@ -92,6 +92,7 @@ public function load(array $configs, ContainerBuilder $container) */ private function handleConfigNode(ContainerBuilder $container, array $config) { + $storageManager = $container->getDefinition('php_translation.storage_manager'); $configurationManager = $container->getDefinition('php_translation.configuration_manager'); // $first will be the "default" configuration. $first = null; @@ -117,6 +118,7 @@ private function handleConfigNode(ContainerBuilder $container, array $config) $storageDefinition->replaceArgument(2, new Reference($configurationServiceId)); $storageDefinition->setPublic(true); $container->setDefinition('php_translation.storage.'.$name, $storageDefinition); + $storageManager->addMethodCall('addStorage', [$name, new Reference('php_translation.storage.'.$name)]); // Add storages foreach ($c['remote_storage'] as $serviceId) { diff --git a/Resources/config/console.yml b/Resources/config/console.yml index 60f25741..956eb4d2 100644 --- a/Resources/config/console.yml +++ b/Resources/config/console.yml @@ -2,25 +2,50 @@ services: php_translator.console.delete_obsolete: class: Translation\Bundle\Command\DeleteObsoleteCommand public: true + arguments: + - '@php_translation.storage_manager' + - '@php_translation.configuration_manager' + - '@php_translation.catalogue_manager' + - '@php_translation.catalogue_fetcher' tags: - { name: console.command, command: translation:delete-obsolete} + php_translator.console.download: class: Translation\Bundle\Command\DownloadCommand public: true + arguments: + - '@php_translation.storage_manager' + - '@php_translation.configuration_manager' + - '@php_translation.cache_clearer' tags: - { name: console.command, command: translation:download } + php_translator.console.extract: class: Translation\Bundle\Command\ExtractCommand public: true + arguments: + - '@php_translation.catalogue_fetcher' + - '@php_translation.catalogue_writer' + - '@php_translation.catalogue_counter' + - '@php_translation.importer' + - '@php_translation.configuration_manager' tags: - { name: console.command, command: translation:extract } + php_translator.console.status: class: Translation\Bundle\Command\StatusCommand public: true + arguments: + - '@php_translation.catalogue_counter' + - '@php_translation.configuration_manager' + - '@php_translation.catalogue_fetcher' tags: - { name: console.command, command: translation:status } + php_translator.console.sync: class: Translation\Bundle\Command\SyncCommand public: true + arguments: + - '@php_translation.storage_manager' tags: - { name: console.command, command: translation:sync } diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 732e93cf..3c8c913d 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -21,6 +21,10 @@ services: php_translation.extractor: class: Translation\Extractor\Extractor + php_translation.storage_manager: + public: true + class: Translation\Bundle\Service\StorageManager + php_translation.configuration_manager: public: true class: Translation\Bundle\Service\ConfigurationManager diff --git a/Service/StorageManager.php b/Service/StorageManager.php new file mode 100644 index 00000000..98a93ae3 --- /dev/null +++ b/Service/StorageManager.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Service; + +/** + * A service to easily access different storage services. + * + * @author Tobias Nyholm + */ +final class StorageManager +{ + /** + * @var StorageService[] + */ + private $storages = []; + + /** + * @param string $name + * @param StorageService $storage + */ + public function addStorage($name, StorageService $storage) + { + $this->storages[$name] = $storage; + } + + /** + * @param string $name + * + * @return null|StorageService + */ + public function getStorage($name = null) + { + if (empty($name)) { + return $this->getStorage('default'); + } + + if (isset($this->storages[$name])) { + return $this->storages[$name]; + } + + if ('default' === $name) { + $name = $this->getFirstName(); + if (isset($this->storages[$name])) { + return $this->storages[$name]; + } + } + } + + /** + * @return string|null + */ + public function getFirstName() + { + foreach ($this->storages as $name => $config) { + return $name; + } + } + + /** + * @return array + */ + public function getNames() + { + return array_keys($this->storages); + } +} diff --git a/Tests/Functional/Command/ExtractCommandTest.php b/Tests/Functional/Command/ExtractCommandTest.php index 35da666a..963255b8 100644 --- a/Tests/Functional/Command/ExtractCommandTest.php +++ b/Tests/Functional/Command/ExtractCommandTest.php @@ -13,7 +13,6 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; -use Translation\Bundle\Command\ExtractCommand; use Translation\Bundle\Model\Metadata; use Translation\Bundle\Tests\Functional\BaseTestCase; @@ -67,7 +66,8 @@ public function testExecute() $this->bootKernel(); $application = new Application($this->kernel); - $application->add(new ExtractCommand()); + $container = $this->getContainer(); + $application->add($container->get('php_translator.console.extract')); $command = $application->find('translation:extract'); $commandTester = new CommandTester($command); diff --git a/Tests/Functional/Command/StatusCommandTest.php b/Tests/Functional/Command/StatusCommandTest.php index 36b32b1a..0de0ccb1 100644 --- a/Tests/Functional/Command/StatusCommandTest.php +++ b/Tests/Functional/Command/StatusCommandTest.php @@ -13,7 +13,6 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; -use Translation\Bundle\Command\StatusCommand; use Translation\Bundle\Tests\Functional\BaseTestCase; class StatusCommandTest extends BaseTestCase @@ -29,7 +28,8 @@ public function testExecute() $this->bootKernel(); $application = new Application($this->kernel); - $application->add(new StatusCommand()); + $container = $this->getContainer(); + $application->add($container->get('php_translator.console.status')); $command = $application->find('translation:status'); $commandTester = new CommandTester($command); From cb44bc99820736c2467fe1b7b993f58b68873445 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 12 Feb 2018 17:45:23 +0100 Subject: [PATCH 031/234] Adding argument for direction (#197) * Adding argument for direction * cs --- Command/SyncCommand.php | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Command/SyncCommand.php b/Command/SyncCommand.php index dccaf929..9fc97be2 100644 --- a/Command/SyncCommand.php +++ b/Command/SyncCommand.php @@ -16,6 +16,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Translation\Bundle\Service\StorageManager; +use Translation\Bundle\Service\StorageService; /** * @author Tobias Nyholm @@ -44,12 +45,28 @@ protected function configure() $this ->setName(self::$defaultName) ->setDescription('Sync the translations with the remote storage') - ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default'); + ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') + ->addArgument('direction', InputArgument::OPTIONAL, 'Use "down" if local changes should be overwritten', 'down'); } protected function execute(InputInterface $input, OutputInterface $output) { + switch ($input->getArgument('direction')) { + case 'down': + $direction = StorageService::DIRECTION_DOWN; + + break; + case 'up': + $direction = StorageService::DIRECTION_UP; + + break; + default: + $output->writeln(sprintf('Direction must be eitehr "up" or "down". Not "%s".', $input->getArgument('direction'))); + + return; + } + $configName = $input->getArgument('configuration'); - $this->storageManager->getStorage($configName)->sync(); + $this->storageManager->getStorage($configName)->sync($direction); } } From 6cbd0edc6cca808ead9c90e43b371e1b836dd758 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 12 Feb 2018 18:00:50 +0100 Subject: [PATCH 032/234] Prepare for 0.6.2 (#193) * Prepare for 0.6.1 * Added more changelogs * Added changelog --- Changelog.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Changelog.md b/Changelog.md index 4f85ac2f..e3adb05c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,30 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.6.2 + +### Added + +- User feedback when you use DeleteObsoleteCommand. +- Injecet depedencies in commands. +- Added argument for sync direction. + +### Changed + +- The service `php_translation.storage.default` is now public. +- The XliffDumper does not backup existing files before creating dump. This is the default behavior in + Symfony 4. + +### Fixed + +- `Metadata::$notes` will not change when running `Metadata::getAllInCategory()` + +## 0.6.1 + +### Fixed + +-- Symfony 4 issues with the DownloadCommand. + ## 0.6.0 ### Added From 94a02481edd80bbac07bd65008f4ffc69b567a3e Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Wed, 21 Feb 2018 12:13:58 +0200 Subject: [PATCH 033/234] Fix a minor typo --- Command/SyncCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Command/SyncCommand.php b/Command/SyncCommand.php index 9fc97be2..461ba6f5 100644 --- a/Command/SyncCommand.php +++ b/Command/SyncCommand.php @@ -61,7 +61,7 @@ protected function execute(InputInterface $input, OutputInterface $output) break; default: - $output->writeln(sprintf('Direction must be eitehr "up" or "down". Not "%s".', $input->getArgument('direction'))); + $output->writeln(sprintf('Direction must be either "up" or "down". Not "%s".', $input->getArgument('direction'))); return; } From 51c937782a393f83e70a86a5ef046527d9c2d12c Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 10 Apr 2018 21:08:46 +0200 Subject: [PATCH 034/234] Using MessageInterface (#191) * Using MessageInterface * Renamed variables * Update dependencies * cs * Bugfix --- Controller/EditInPlaceController.php | 9 +++----- Controller/SymfonyProfilerController.php | 14 ++++++------ Controller/WebUIController.php | 28 +++++++++++------------- Model/SfProfilerMessage.php | 5 +++-- Service/StorageService.php | 11 +++++----- composer.json | 4 ++-- 6 files changed, 34 insertions(+), 37 deletions(-) diff --git a/Controller/EditInPlaceController.php b/Controller/EditInPlaceController.php index f3f7395f..42f25e8c 100644 --- a/Controller/EditInPlaceController.php +++ b/Controller/EditInPlaceController.php @@ -17,6 +17,7 @@ use Translation\Bundle\Exception\MessageValidationException; use Translation\Bundle\Service\StorageService; use Translation\Common\Model\Message; +use Translation\Common\Model\MessageInterface; /** * @author Damien Alexandre @@ -57,7 +58,7 @@ public function editAction(Request $request, $configName, $locale) * @param string $locale * @param array $validationGroups * - * @return Message[] + * @return MessageInterface[] * * @throws MessageValidationException */ @@ -71,11 +72,7 @@ private function getMessages(Request $request, $locale, array $validationGroups foreach ($data as $key => $value) { list($domain, $translationKey) = explode('|', $key); - $message = new Message(); - $message->setKey($translationKey); - $message->setTranslation($value); - $message->setDomain($domain); - $message->setLocale($locale); + $message = new Message($translationKey, $domain, $locale, $value); $errors = $validator->validate($message, null, $validationGroups); if (count($errors) > 0) { diff --git a/Controller/SymfonyProfilerController.php b/Controller/SymfonyProfilerController.php index 8f659f78..084ec4fa 100644 --- a/Controller/SymfonyProfilerController.php +++ b/Controller/SymfonyProfilerController.php @@ -18,7 +18,7 @@ use Symfony\Component\VarDumper\Cloner\Data; use Translation\Bundle\Model\SfProfilerMessage; use Translation\Bundle\Service\StorageService; -use Translation\Common\Model\Message; +use Translation\Common\Model\MessageInterface; /** * @author Tobias Nyholm @@ -154,16 +154,16 @@ private function getMessage(Request $request, $token) throw $this->createNotFoundException('No collector with name "translation" was found.'); } - $messages = $dataCollector->getMessages(); + $collectorMessages = $dataCollector->getMessages(); - if ($messages instanceof Data) { - $messages = $messages->getValue(true); + if ($collectorMessages instanceof Data) { + $collectorMessages = $collectorMessages->getValue(true); } - if (!isset($messages[$messageId])) { + if (!isset($collectorMessages[$messageId])) { throw $this->createNotFoundException(sprintf('No message with key "%s" was found.', $messageId)); } - $message = SfProfilerMessage::create($messages[$messageId]); + $message = SfProfilerMessage::create($collectorMessages[$messageId]); if (DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK === $message->getState()) { $message->setLocale($profile->getCollector('request')->getLocale()) @@ -177,7 +177,7 @@ private function getMessage(Request $request, $token) * @param Request $request * @param string $token * - * @return Message[] + * @return MessageInterface[] */ protected function getSelectedMessages(Request $request, $token) { diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index f397b540..8a197f26 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -22,6 +22,7 @@ use Translation\Common\Exception\StorageException; use Translation\Bundle\Model\CatalogueMessage; use Translation\Common\Model\Message; +use Translation\Common\Model\MessageInterface; /** * @author Tobias Nyholm @@ -141,8 +142,8 @@ public function createAction(Request $request, $configName, $locale, $domain) try { $message = $this->getMessageFromRequest($request); - $message->setDomain($domain); - $message->setLocale($locale); + $message = $message->withDomain($domain); + $message = $message->withLocale($locale); $this->validateMessage($message, ['Create']); } catch (MessageValidationException $e) { return new Response($e->getMessage(), 400); @@ -180,8 +181,8 @@ public function editAction(Request $request, $configName, $locale, $domain) try { $message = $this->getMessageFromRequest($request); - $message->setDomain($domain); - $message->setLocale($locale); + $message = $message->withDomain($domain); + $message = $message->withLocale($locale); $this->validateMessage($message, ['Edit']); } catch (MessageValidationException $e) { return new Response($e->getMessage(), 400); @@ -210,8 +211,8 @@ public function deleteAction(Request $request, $configName, $locale, $domain) try { $message = $this->getMessageFromRequest($request); - $message->setLocale($locale); - $message->setDomain($domain); + $message = $message->withLocale($locale); + $message = $message->withDomain($domain); $this->validateMessage($message, ['Delete']); } catch (MessageValidationException $e) { return new Response($e->getMessage(), 400); @@ -227,18 +228,15 @@ public function deleteAction(Request $request, $configName, $locale, $domain) /** * @param Request $request * - * @return Message + * @return MessageInterface */ private function getMessageFromRequest(Request $request) { $json = $request->getContent(); $data = json_decode($json, true); - $message = new Message(); - if (isset($data['key'])) { - $message->setKey($data['key']); - } + $message = new Message($data['key']); if (isset($data['message'])) { - $message->setTranslation($data['message']); + $message = $message->withTranslation($data['message']); } return $message; @@ -262,12 +260,12 @@ private function getLocale2LanguageMap() } /** - * @param Message $message - * @param array $validationGroups + * @param MessageInterface $message + * @param array $validationGroups * * @throws MessageValidationException */ - private function validateMessage(Message $message, array $validationGroups) + private function validateMessage(MessageInterface $message, array $validationGroups) { $errors = $this->get('validator')->validate($message, null, $validationGroups); if (count($errors) > 0) { diff --git a/Model/SfProfilerMessage.php b/Model/SfProfilerMessage.php index e3079c22..ede04f44 100644 --- a/Model/SfProfilerMessage.php +++ b/Model/SfProfilerMessage.php @@ -13,6 +13,7 @@ use Symfony\Component\VarDumper\Cloner\Data; use Translation\Common\Model\Message; +use Translation\Common\Model\MessageInterface; /** * @author Tobias Nyholm @@ -118,9 +119,9 @@ public static function create(array $data) } /** - * Convert to a Common\Message. + * Convert to a Common\Model\MessageInterface. * - * @return Message + * @return MessageInterface */ public function convertToMessage() { diff --git a/Service/StorageService.php b/Service/StorageService.php index 5b4a6984..72046abe 100644 --- a/Service/StorageService.php +++ b/Service/StorageService.php @@ -17,6 +17,7 @@ use Translation\Bundle\Model\Configuration; use Translation\Common\Exception\LogicException; use Translation\Common\Model\Message; +use Translation\Common\Model\MessageInterface; use Translation\Common\Storage; use Translation\Common\TransferableStorage; @@ -208,7 +209,7 @@ private function getFromStorages(array $storages, $locale, $domain, $key) * * {@inheritdoc} */ - public function create(Message $message) + public function create(MessageInterface $message) { // Validate if message actually has data if (empty((array) $message)) { @@ -229,7 +230,7 @@ public function create(Message $message) * * {@inheritdoc} */ - public function update(Message $message) + public function update(MessageInterface $message) { foreach ([$this->localStorages, $this->remoteStorages] as $storages) { $this->updateStorages($storages, $message); @@ -237,10 +238,10 @@ public function update(Message $message) } /** - * @param Storage[] $storages - * @param Message $message + * @param Storage[] $storages + * @param MessageInterface $message */ - private function updateStorages(array $storages, Message $message) + private function updateStorages(array $storages, MessageInterface $message) { // Validate if message actually has data if (empty((array) $message)) { diff --git a/composer.json b/composer.json index 970de800..1e1fda6c 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,8 @@ "symfony/finder": "^2.7 || ^3.0 || ^4.0", "symfony/intl": "^2.7 || ^3.0 || ^4.0", - "php-translation/common": "^0.2.1", - "php-translation/symfony-storage": "^0.4.0", + "php-translation/common": "^0.3", + "php-translation/symfony-storage": "^0.5.0", "php-translation/extractor": "^1.3" }, "require-dev": { From a0b36959fa525f65354d96f4b54803815c9bb325 Mon Sep 17 00:00:00 2001 From: Karol Grochowalski Date: Tue, 10 Apr 2018 21:39:32 +0200 Subject: [PATCH 035/234] 'po' file format support to extractor (#204) --- DependencyInjection/Configuration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index af75001c..c36abbed 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -141,7 +141,7 @@ private function configsNode(ArrayNodeDefinition $root) ->arrayNode('external_translations_dirs') ->prototype('scalar')->end() ->end() - ->enumNode('output_format')->values(['php', 'yml', 'xlf'])->defaultValue('xlf')->end() + ->enumNode('output_format')->values(['php', 'yml', 'xlf', 'po'])->defaultValue('xlf')->end() ->arrayNode('blacklist_domains') ->prototype('scalar')->end() ->end() From b98a193e18e22573719b600aeb93419e5f310a09 Mon Sep 17 00:00:00 2001 From: DPvic Date: Tue, 10 Apr 2018 21:59:36 +0200 Subject: [PATCH 036/234] Fixed Edit in place twig extension activator (#200) * Fixed Edit in place twig extension activator * Minor fixes --- DependencyInjection/TranslationExtension.php | 3 +++ Resources/config/edit_in_place.yml | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/DependencyInjection/TranslationExtension.php b/DependencyInjection/TranslationExtension.php index b7848507..dec5c333 100644 --- a/DependencyInjection/TranslationExtension.php +++ b/DependencyInjection/TranslationExtension.php @@ -196,6 +196,9 @@ private function enableEditInPlace(ContainerBuilder $container, array $config) $def = $container->getDefinition('php_translator.edit_in_place.xtrans_html_translator'); $def->replaceArgument(1, $activatorRef); + + $def = $container->getDefinition('php_translation.edit_in_place.extension.trans'); + $def->addMethodCall('setActivator', [$activatorRef]); } /** diff --git a/Resources/config/edit_in_place.yml b/Resources/config/edit_in_place.yml index 23174c11..57690a06 100644 --- a/Resources/config/edit_in_place.yml +++ b/Resources/config/edit_in_place.yml @@ -28,7 +28,6 @@ services: arguments: - '@php_translator.edit_in_place.xtrans_html_translator' calls: - - [setActivator, ['@php_translation.edit_in_place.activator']] - [setRequestStack, ['@request_stack']] tags: - { name: 'twig.extension' } From ef84ff7162efd7f18bb54574825b48f91bc0a7b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Andrieux?= Date: Tue, 10 Apr 2018 22:10:31 +0200 Subject: [PATCH 037/234] Remove useless array wrapping (#203) We already check that it's an array in the configuration definition. This wrapping is useless and prevent proper options retrieving. --- DependencyInjection/TranslationExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DependencyInjection/TranslationExtension.php b/DependencyInjection/TranslationExtension.php index dec5c333..2171bb5a 100644 --- a/DependencyInjection/TranslationExtension.php +++ b/DependencyInjection/TranslationExtension.php @@ -134,7 +134,7 @@ private function handleConfigNode(ContainerBuilder $container, array $config) $def = $this->createChildDefinition($serviceId); $def->replaceArgument(2, [$c['output_dir']]) - ->replaceArgument(3, [$c['local_file_storage_options']]) + ->replaceArgument(3, $c['local_file_storage_options']) ->addTag('php_translation.storage', ['type' => 'local', 'name' => $name]); $container->setDefinition('php_translation.single_storage.file.'.$name, $def); } From 3674b79d5d27e8ff91a29b487015b88c568191e5 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Wed, 11 Apr 2018 05:57:18 +0200 Subject: [PATCH 038/234] Increase max execution time (#217) * Increase max execution time * fixed typo --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 39999d72..1294fdf0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,6 +52,8 @@ matrix: - env: STABILITY="dev" before_install: + - INI_FILE=~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini + - if [[ $COVERAGE == true ]]; then echo max_execution_time = 600 >> $INI_FILE; fi - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi - if ! [ -z "$STABILITY" ]; then composer config minimum-stability ${STABILITY}; fi; - if ! [ -z "$DEPENDENCIES" ]; then composer require --no-update ${DEPENDENCIES}; fi; From 0e13cf69cb1e3cc655179d98fd474e849a9f8402 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Wed, 11 Apr 2018 10:05:31 +0200 Subject: [PATCH 039/234] Use "desc" as default translation when extracting (#218) * Simplify code * bugfix * Disable DefaultApplyingNodeVisitor and RemovingNodeVisitor when extracting translations * Use "desc" as default translation when extracting * cs --- Model/Metadata.php | 15 +++ Resources/config/services.yml | 2 +- Service/Importer.php | 31 +++++- Twig/Visitor/DefaultApplyingNodeVisitor.php | 110 ++++++++++---------- 4 files changed, 101 insertions(+), 57 deletions(-) diff --git a/Model/Metadata.php b/Model/Metadata.php index 6cd0d4b2..5be4e6c4 100644 --- a/Model/Metadata.php +++ b/Model/Metadata.php @@ -65,6 +65,21 @@ public function setState($state) $this->addCategory('state', $state); } + /** + * @return null|string + */ + public function getDesc() + { + $notes = $this->getAllInCategory('desc'); + foreach ($notes as $note) { + if (isset($note['content'])) { + return $note['content']; + } + } + + return null; + } + /** * @return bool */ diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 3c8c913d..355ca6ec 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -32,7 +32,7 @@ services: php_translation.importer: public: true class: Translation\Bundle\Service\Importer - arguments: ["@php_translation.extractor"] + arguments: ["@php_translation.extractor", "@twig"] php_translation.cache_clearer: public: true diff --git a/Service/Importer.php b/Service/Importer.php index 1760b77d..f4f1a118 100644 --- a/Service/Importer.php +++ b/Service/Importer.php @@ -15,6 +15,8 @@ use Symfony\Component\Translation\MessageCatalogue; use Translation\Bundle\Model\ImportResult; use Translation\Bundle\Model\Metadata; +use Translation\Bundle\Twig\Visitor\DefaultApplyingNodeVisitor; +use Translation\Bundle\Twig\Visitor\RemovingNodeVisitor; use Translation\Extractor\Extractor; use Translation\Extractor\Model\SourceCollection; use Translation\Extractor\Model\SourceLocation; @@ -38,11 +40,18 @@ final class Importer private $config; /** - * @param Extractor $extractor + * @var \Twig_Environment */ - public function __construct(Extractor $extractor) + private $twig; + + /** + * @param Extractor $extractor + * @param \Twig_Environment $twig + */ + public function __construct(Extractor $extractor, \Twig_Environment $twig) { $this->extractor = $extractor; + $this->twig = $twig; } /** @@ -60,6 +69,7 @@ public function __construct(Extractor $extractor) public function extractToCatalogues(Finder $finder, array $catalogues, array $config = []) { $this->processConfig($config); + $this->disableTwigVisitors(); $sourceCollection = $this->extractor->extract($finder); $results = []; foreach ($catalogues as $catalogue) { @@ -86,6 +96,11 @@ public function extractToCatalogues(Finder $finder, array $catalogues, array $co $meta = $this->getMetadata($result, $key, $domain); $meta->setState('new'); $this->setMetadata($result, $key, $domain, $meta); + + // Add "desc" as translation + if (null === $translation && null !== $desc = $meta->getDesc()) { + $result->set($key, $desc, $domain); + } } foreach ($merge->getObsoleteMessages($domain) as $key => $translation) { $meta = $this->getMetadata($result, $key, $domain); @@ -196,4 +211,16 @@ private function processConfig($config) $this->config = $config; } + + private function disableTwigVisitors() + { + foreach ($this->twig->getNodeVisitors() as $visitor) { + if ($visitor instanceof DefaultApplyingNodeVisitor) { + $visitor->setEnabled(false); + } + if ($visitor instanceof RemovingNodeVisitor) { + $visitor->setEnabled(false); + } + } + } } diff --git a/Twig/Visitor/DefaultApplyingNodeVisitor.php b/Twig/Visitor/DefaultApplyingNodeVisitor.php index a6618eaa..d097e016 100644 --- a/Twig/Visitor/DefaultApplyingNodeVisitor.php +++ b/Twig/Visitor/DefaultApplyingNodeVisitor.php @@ -48,68 +48,70 @@ public function doEnterNode(\Twig_Node $node, \Twig_Environment $env) return $node; } - if ($node instanceof \Twig_Node_Expression_Filter && 'desc' === $node->getNode('filter')->getAttribute('value')) { - $transNode = $node->getNode('node'); - while ($transNode instanceof \Twig_Node_Expression_Filter - && 'trans' !== $transNode->getNode('filter')->getAttribute('value') - && 'transchoice' !== $transNode->getNode('filter')->getAttribute('value')) { - $transNode = $transNode->getNode('node'); - } + if (!($node instanceof \Twig_Node_Expression_Filter && 'desc' === $node->getNode('filter')->getAttribute('value'))) { + return $node; + } - if (!$transNode instanceof \Twig_Node_Expression_Filter) { - throw new \RuntimeException(sprintf('The "desc" filter must be applied after a "trans", or "transchoice" filter.')); - } + $transNode = $node->getNode('node'); + while ($transNode instanceof \Twig_Node_Expression_Filter + && 'trans' !== $transNode->getNode('filter')->getAttribute('value') + && 'transchoice' !== $transNode->getNode('filter')->getAttribute('value')) { + $transNode = $transNode->getNode('node'); + } - $wrappingNode = $node->getNode('node'); - $testNode = clone $wrappingNode; - $defaultNode = $node->getNode('arguments')->getNode(0); - - // if the |transchoice filter is used, delegate the call to the TranslationExtension - // so that we can catch a possible exception when the default translation has not yet - // been extracted - if ('transchoice' === $transNode->getNode('filter')->getAttribute('value')) { - $transchoiceArguments = new \Twig_Node_Expression_Array([], $transNode->getTemplateLine()); - $transchoiceArguments->addElement($wrappingNode->getNode('node')); - $transchoiceArguments->addElement($defaultNode); - foreach ($wrappingNode->getNode('arguments') as $arg) { - $transchoiceArguments->addElement($arg); - } - - $transchoiceNode = new Transchoice($transchoiceArguments, $transNode->getTemplateLine()); - $node->setNode('node', $transchoiceNode); - - return $node; - } + if (!$transNode instanceof \Twig_Node_Expression_Filter) { + throw new \RuntimeException(sprintf('The "desc" filter must be applied after a "trans", or "transchoice" filter.')); + } - // if the |trans filter has replacements parameters - // (e.g. |trans({'%foo%': 'bar'})) - if ($wrappingNode->getNode('arguments')->hasNode(0)) { - $lineno = $wrappingNode->getTemplateLine(); - - // remove the replacements from the test node - $testNode->setNode('arguments', clone $testNode->getNode('arguments')); - $testNode->getNode('arguments')->setNode(0, new \Twig_Node_Expression_Array([], $lineno)); - - // wrap the default node in a |replace filter - $defaultNode = new \Twig_Node_Expression_Filter( - clone $node->getNode('arguments')->getNode(0), - new \Twig_Node_Expression_Constant('replace', $lineno), - new \Twig_Node([ - clone $wrappingNode->getNode('arguments')->getNode(0), - ]), - $lineno - ); + $wrappingNode = $node->getNode('node'); + $testNode = clone $wrappingNode; + $defaultNode = $node->getNode('arguments')->getNode(0); + + // if the |transchoice filter is used, delegate the call to the TranslationExtension + // so that we can catch a possible exception when the default translation has not yet + // been extracted + if ('transchoice' === $transNode->getNode('filter')->getAttribute('value')) { + $transchoiceArguments = new \Twig_Node_Expression_Array([], $transNode->getTemplateLine()); + $transchoiceArguments->addElement($wrappingNode->getNode('node')); + $transchoiceArguments->addElement($defaultNode); + foreach ($wrappingNode->getNode('arguments') as $arg) { + $transchoiceArguments->addElement($arg); } - $condition = new \Twig_Node_Expression_Conditional( - new \Twig_Node_Expression_Binary_Equal($testNode, $transNode->getNode('node'), $wrappingNode->getTemplateLine()), - $defaultNode, - clone $wrappingNode, - $wrappingNode->getTemplateLine() + $transchoiceNode = new Transchoice($transchoiceArguments, $transNode->getTemplateLine()); + $node->setNode('node', $transchoiceNode); + + return $node; + } + + // if the |trans filter has replacements parameters + // (e.g. |trans({'%foo%': 'bar'})) + if ($wrappingNode->getNode('arguments')->hasNode(0)) { + $lineno = $wrappingNode->getTemplateLine(); + + // remove the replacements from the test node + $testNode->setNode('arguments', clone $testNode->getNode('arguments')); + $testNode->getNode('arguments')->setNode(0, new \Twig_Node_Expression_Array([], $lineno)); + + // wrap the default node in a |replace filter + $defaultNode = new \Twig_Node_Expression_Filter( + clone $node->getNode('arguments')->getNode(0), + new \Twig_Node_Expression_Constant('replace', $lineno), + new \Twig_Node([ + clone $wrappingNode->getNode('arguments')->getNode(0), + ]), + $lineno ); - $node->setNode('node', $condition); } + $condition = new \Twig_Node_Expression_Conditional( + new \Twig_Node_Expression_Binary_Equal($testNode, $transNode->getNode('node'), $wrappingNode->getTemplateLine()), + $defaultNode, + clone $wrappingNode, + $wrappingNode->getTemplateLine() + ); + $node->setNode('node', $condition); + return $node; } From da6bb44515f350766e1c55ba90c20e28f19463da Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Wed, 11 Apr 2018 16:45:46 +0200 Subject: [PATCH 040/234] Require TwigBundle (#221) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1e1fda6c..56f9027b 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "symfony/framework-bundle": "^2.7 || ^3.0 || ^4.0", "symfony/validator": "^2.7 || ^3.0 || ^4.0", "symfony/translation": "^2.7 || ^3.0 || ^4.0", + "symfony/twig-bundle": "^2.7 || ^3.0 || ^4.0", "symfony/finder": "^2.7 || ^3.0 || ^4.0", "symfony/intl": "^2.7 || ^3.0 || ^4.0", @@ -28,7 +29,6 @@ "php-http/message": "^1.6", "php-http/message-factory": "^1.0.2", "symfony/console": "^2.7 || ^3.0 || ^4.0", - "symfony/twig-bundle": "^2.7 || ^3.0 || ^4.0", "symfony/twig-bridge": "^2.7 || ^3.0 || ^4.0", "symfony/asset": "^2.7 || ^3.0 || ^4.0", "symfony/templating": "^2.7 || ^3.0 || ^4.0", From b1a77c64a2c530ac843ffd7c528234706dbb105e Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Wed, 11 Apr 2018 16:52:47 +0200 Subject: [PATCH 041/234] Add support for adding translation on SourceLocation (#219) --- Model/Metadata.php | 17 +++++++++++++++++ Service/Importer.php | 15 ++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Model/Metadata.php b/Model/Metadata.php index 5be4e6c4..4fba5d23 100644 --- a/Model/Metadata.php +++ b/Model/Metadata.php @@ -80,6 +80,23 @@ public function getDesc() return null; } + /** + * Get the extracted translation if any. + * + * @return null|string + */ + public function getTranslation() + { + $notes = $this->getAllInCategory('translation'); + foreach ($notes as $note) { + if (isset($note['content'])) { + return $note['content']; + } + } + + return null; + } + /** * @return bool */ diff --git a/Service/Importer.php b/Service/Importer.php index f4f1a118..ad0287a0 100644 --- a/Service/Importer.php +++ b/Service/Importer.php @@ -97,9 +97,15 @@ public function extractToCatalogues(Finder $finder, array $catalogues, array $co $meta->setState('new'); $this->setMetadata($result, $key, $domain, $meta); - // Add "desc" as translation - if (null === $translation && null !== $desc = $meta->getDesc()) { - $result->set($key, $desc, $domain); + // Add custom translations that we found in the source + if (null === $translation) { + if (null !== $newTranslation = $meta->getTranslation()) { + $result->set($key, $newTranslation, $domain); + // We do not want "translation" key stored anywhere. + $meta->removeAllInCategory('translation'); + } elseif (null !== $newTranslation = $meta->getDesc()) { + $result->set($key, $newTranslation, $domain); + } } } foreach ($merge->getObsoleteMessages($domain) as $key => $translation) { @@ -138,6 +144,9 @@ private function convertSourceLocationsToMessages(MessageCatalogue $catalogue, S if (isset($sourceLocation->getContext()['desc'])) { $meta->addCategory('desc', $sourceLocation->getContext()['desc']); } + if (isset($sourceLocation->getContext()['translation'])) { + $meta->addCategory('translation', $sourceLocation->getContext()['translation']); + } $this->setMetadata($catalogue, $key, $domain, $meta); } } From 92d74ea090b8758886d766b5b9882880edf6e891 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 12 Apr 2018 08:00:43 +0200 Subject: [PATCH 042/234] Better respect blacklist and whitelist (#220) * Better respect blacklist and whitelist * travis fix * Move around builds --- .travis.yml | 9 +++++---- Catalogue/CatalogueFetcher.php | 31 +++++++++++++++++++++++++++++++ composer.json | 4 ++-- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1294fdf0..27b527c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,10 @@ env: matrix: fast_finish: true include: + # Run test with code coverage + - php: 7.2 + env: COVERAGE=true TEST_COMMAND="composer test-ci" + # Test with lowest dependencies - php: 7.1 env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" SYMFONY_DEPRECATIONS_HELPER="weak" SYMFONY_PHPUNIT_VERSION="5.7" @@ -30,8 +34,6 @@ matrix: env: SYMFONY_PHPUNIT_VERSION="5.7" - php: 7.0 - php: 7.1 - - php: 7.2 - env: COVERAGE=true TEST_COMMAND="composer test-ci" # Force some major versions of Symfony - php: 7.2 @@ -52,8 +54,7 @@ matrix: - env: STABILITY="dev" before_install: - - INI_FILE=~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini - - if [[ $COVERAGE == true ]]; then echo max_execution_time = 600 >> $INI_FILE; fi + - if [[ $COVERAGE == true ]]; then echo "max_execution_time = 600" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; fi - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi - if ! [ -z "$STABILITY" ]; then composer config minimum-stability ${STABILITY}; fi; - if ! [ -z "$DEPENDENCIES" ]; then composer require --no-update ${DEPENDENCIES}; fi; diff --git a/Catalogue/CatalogueFetcher.php b/Catalogue/CatalogueFetcher.php index 4b666307..3ef2e093 100644 --- a/Catalogue/CatalogueFetcher.php +++ b/Catalogue/CatalogueFetcher.php @@ -11,6 +11,7 @@ namespace Translation\Bundle\Catalogue; +use Nyholm\NSA; use Symfony\Bundle\FrameworkBundle\Translation\TranslationLoader as SymfonyTranslationLoader; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Reader\TranslationReaderInterface; @@ -67,9 +68,39 @@ public function getCatalogues(Configuration $config, array $locales = []) $this->reader->read($path, $currentCatalogue); } } + + foreach ($currentCatalogue->getDomains() as $domain) { + if (!$this->isValidDomain($config, $domain)) { + $messages = $currentCatalogue->all(); + unset($messages[$domain]); + NSA::setProperty($currentCatalogue, 'messages', $messages); + } + } + $catalogues[] = $currentCatalogue; } return $catalogues; } + + /** + * @param string $domain + * + * @return bool + */ + private function isValidDomain(Configuration $config, $domain) + { + $blacklist = $config->getBlacklistDomains(); + $whitelist = $config->getWhitelistDomains(); + + if (!empty($blacklist) && in_array($domain, $blacklist)) { + return false; + } + + if (!empty($whitelist) && !in_array($domain, $whitelist)) { + return false; + } + + return true; + } } diff --git a/composer.json b/composer.json index 56f9027b..f8cb3ad9 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "php-translation/common": "^0.3", "php-translation/symfony-storage": "^0.5.0", - "php-translation/extractor": "^1.3" + "php-translation/extractor": "^1.3", + "nyholm/nsa": "^1.1" }, "require-dev": { "symfony/phpunit-bridge": "^3.4 || ^4.0", @@ -37,7 +38,6 @@ "matthiasnoback/symfony-dependency-injection-test": "^1.2 || ^2.3", "matthiasnoback/symfony-config-test": "^2.2 || ^3.1", "guzzlehttp/psr7": "^1.4", - "nyholm/nsa": "^1.1", "nyholm/symfony-bundle-test": "^1.2.3" }, "suggest": { From 9f7ce4ac0c077d8f58753ac0215fb509406e45e8 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 12 Apr 2018 08:01:41 +0200 Subject: [PATCH 043/234] Prepae for release 0.7.0 (#216) * Added changelog for 0.7.0 * Added note about desc filter * Update Changelog.md --- Changelog.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Changelog.md b/Changelog.md index e3adb05c..8fb06fb9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,24 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.7.0 + +### Added + +- Support for `php-translation/common:0.3` and `php-translation/symfony-storage:0.5` +- Support for dumping to .po files. +- Support for `SourceLocation`'s context key `translation` which adds a default translation to the `Message`. +- Better respect blacklist and whitelist in `CatalogueFetcher`. + +### Fixed + +- Bug with config option `local_file_storage_options` not being used. +- Bug with edit-in-place and custom activator. + +### Changed + +- The "desc" filter will be used as default translation when extracting. + ## 0.6.2 ### Added From daad67076feb17aa5a96b02453375a0991f6f3e0 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 22 May 2018 14:03:10 +0300 Subject: [PATCH 044/234] Make sure we got a locale before we start to translate (#229) --- Translator/FallbackTranslator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Translator/FallbackTranslator.php b/Translator/FallbackTranslator.php index a8a6446d..69da25ef 100644 --- a/Translator/FallbackTranslator.php +++ b/Translator/FallbackTranslator.php @@ -65,7 +65,7 @@ public function trans($id, array $parameters = [], $domain = null, $locale = nul } $locale = $catalogue->getLocale(); - if ($locale === $this->defaultLocale) { + if (empty($locale) || $locale === $this->defaultLocale) { // we cant do anything... return $id; } From fb70b1dc0526561258640d78976506a4879d30ce Mon Sep 17 00:00:00 2001 From: Fabien Papet Date: Sat, 26 May 2018 16:23:02 +0200 Subject: [PATCH 045/234] Get rid of bootstrap overrides (#230) * Get rid of bootstrap overrides * Update webui.css * Update base.html.twig --- Resources/public/css/webui.css | 6 +++--- Resources/views/WebUI/base.html.twig | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Resources/public/css/webui.css b/Resources/public/css/webui.css index 2a3a4f8b..de461dfe 100644 --- a/Resources/public/css/webui.css +++ b/Resources/public/css/webui.css @@ -1,8 +1,8 @@ /* Navigation */ -.navbar { +.configs { padding: 0 2rem; } -.navbar-nav .nav-text{ +.configs .navbar-nav .nav-text{ font-weight: bold; padding-top: 0.2rem; padding-bottom: 0; @@ -10,7 +10,7 @@ width: 6rem; } -.navbar-nav .nav-link { +.configs .navbar-nav .nav-link { padding-top: 0.1rem; padding-bottom: 0.1rem; } diff --git a/Resources/views/WebUI/base.html.twig b/Resources/views/WebUI/base.html.twig index b559301a..a2e9c493 100644 --- a/Resources/views/WebUI/base.html.twig +++ b/Resources/views/WebUI/base.html.twig @@ -11,7 +11,7 @@ - {% for key, message in messages %} - + + {% endif %} From cdd2299cf45d11b4d5bc39f03885406c1d61f06f Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Thu, 16 Nov 2023 15:54:39 +0100 Subject: [PATCH 221/234] Fix code styles (#498) --- Catalogue/Operation/ReplaceOperation.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Catalogue/Operation/ReplaceOperation.php b/Catalogue/Operation/ReplaceOperation.php index 5b012e5e..746a6861 100644 --- a/Catalogue/Operation/ReplaceOperation.php +++ b/Catalogue/Operation/ReplaceOperation.php @@ -150,12 +150,12 @@ private function doMergeMetadata(array $source, array $target): array // If both arrays, do recursive call $source[$key] = $this->doMergeMetadata($source[$key], $value); } - // Else, use value form $source + // Else, use value form $source } else { // Add new value $source[$key] = $value; } - // if sequential + // if sequential } elseif (!\in_array($value, $source, true)) { $source[] = $value; } From f075bfb7d944cdaf74ceb9a614c26a8c8fdd5310 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Thu, 16 Nov 2023 15:59:48 +0100 Subject: [PATCH 222/234] Update Changelog.md for 0.14.2 release (#499) --- Changelog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Changelog.md b/Changelog.md index 7558ca6c..c1897019 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,12 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.14.2 + +### Fixed + +* Fixed render error in profiler/translation.html.twig (fixes #496) by @althaus in https://github.com/php-translation/symfony-bundle/pull/497 + ## 0.14.1 ### Fixed From 722e61673af494824b585b32bc619200b28729e4 Mon Sep 17 00:00:00 2001 From: Steffen Persch Date: Fri, 15 Dec 2023 15:18:10 +0100 Subject: [PATCH 223/234] [SF PROFILER] Use fetch instead of Sfjs.request and minor style adjusments to better match the new profiler layout (#500) * use fetch instead of Sfjs.request * adjust profiler styling to match new profiler theme --- Resources/public/js/symfonyProfiler.js | 148 ++++++++---------- .../views/SymfonyProfiler/edit.html.twig | 2 +- .../SymfonyProfiler/translation.html.twig | 2 +- 3 files changed, 67 insertions(+), 85 deletions(-) diff --git a/Resources/public/js/symfonyProfiler.js b/Resources/public/js/symfonyProfiler.js index 1558c039..6cdd57b1 100644 --- a/Resources/public/js/symfonyProfiler.js +++ b/Resources/public/js/symfonyProfiler.js @@ -19,83 +19,76 @@ function syncMessage(key) { var el = document.getElementById(key).getElementsByClassName("translation"); el[0].innerHTML = getLoaderHTML(); - Sfjs.request( - translationSyncUrl, - function(xhr) { - // Success - el[0].innerHTML = xhr.responseText; - - if (xhr.responseText !== "") { - clearState(key); - } - }, - function(xhr) { - // Error - el[0].innerHTML = "Error - Syncing message " + key + ""; - }, - serializeQueryString({message_id: key}), - { method: 'POST' } - ); + fetch(translationSyncUrl, { + method: 'POST', + body: serializeQueryString({message_id: key}), + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/x-www-form-urlencoded', + } + }).then(res => res.text()).then((text) => { + el[0].innerHTML = text; + + if (text !== "") { + clearState(key); + } + }).catch(() => { + el[0].innerHTML = "Error - Syncing message " + key + ""; + }); } function syncAll() { var el = document.getElementById("top-result-area"); el.innerHTML = getLoaderHTML(); - Sfjs.request( - translationSyncAllUrl, - function(xhr) { - // Success - el.innerHTML = xhr.responseText; - }, - function(xhr) { - // Error - el[0].innerHTML = "Error - Syncing all messages"; + fetch(translationSyncAllUrl, { + method: 'POST', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/x-www-form-urlencoded', }, - {}, - { method: 'POST' } - ); + }).then(res => res.text()).then(text => { + el.innerHTML = text; + }).catch(() => { + el[0].innerHTML = "Error - Syncing all messages"; + }); } function getEditForm(key) { var el = document.getElementById(key).getElementsByClassName("translation"); el[0].innerHTML = getLoaderHTML(); - Sfjs.request( - translationEditUrl + "?" + serializeQueryString({message_id: key}), - function(xhr) { - // Success - el[0].innerHTML = xhr.responseText; + fetch(translationEditUrl + "?" + serializeQueryString({message_id: key}), { + headers: { + 'X-Requested-With': 'XMLHttpRequest', }, - function(xhr) { - // Error - el[0].innerHTML = "Error - Getting edit form " + key + ""; - }, - { method: 'GET' } - ); + }).then(res => res.text()).then(text => { + el[0].innerHTML = text; + }).catch(() => { + el[0].innerHTML = "Error - Getting edit form " + key + ""; + }); } function saveEditForm(key, translation) { var el = document.getElementById(key).getElementsByClassName("translation"); el[0].innerHTML = getLoaderHTML(); - Sfjs.request( - translationEditUrl, - function(xhr) { - // Success - el[0].innerHTML = xhr.responseText; - - if (xhr.responseText !== "") { - clearState(key); - } - }, - function(xhr) { - // Error - el[0].innerHTML = "Error - Saving edit form " + key + ""; + fetch(translationEditUrl, { + method: 'POST', + body: serializeQueryString({message_id: key, translation:translation}), + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/x-www-form-urlencoded', }, - serializeQueryString({message_id: key, translation:translation}), - { method: 'POST' } - ); + }).then(res => res.text()).then(text => { + el[0].innerHTML = text; + + if (text !== "") { + clearState(key); + } + }).catch(() => { + el[0].innerHTML = "Error - Saving edit form " + key + ""; + }) return false; } @@ -130,17 +123,6 @@ var serializeQueryString = function(obj, prefix) { return str.join("&"); }; -// We need to hack a bit Sfjs.request because it does not support POST requests -// May not work for ActiveXObject('Microsoft.XMLHTTP'); :( -(function(open) { - XMLHttpRequest.prototype.open = function(method, url, async, user, pass) { - open.call(this, method, url, async, user, pass); - if (method.toLowerCase() === 'post') { - this.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); - } - }; -})(XMLHttpRequest.prototype.open); - var saveTranslations = function(form) { "use strict"; @@ -169,22 +151,22 @@ var saveTranslations = function(form) { el.classList.remove('status-error'); el.classList.remove('status-success'); - Sfjs.request( - form.action, - function(xhr) { - // Success - el.classList.add('label'); - el.classList.add('status-success'); - el.innerHTML = xhr.responseText; + fetch(form.action, { + method: 'POST', + body: serializeQueryString({selected: selected}), + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/x-www-form-urlencoded', }, - function(xhr) { - // Error - el.classList.add('label'); - el.classList.add('status-error'); - el.innerHTML = xhr.responseText; - }, - serializeQueryString({selected: selected}), - { method: 'POST' } - ); + }).then(res => res.text()).then(text => { + el.classList.add('label'); + el.classList.add('status-success'); + el.innerHTML = text; + }).catch(error => { + el.classList.add('label'); + el.classList.add('status-error'); + el.innerHTML = error; + }) + return false; }; diff --git a/Resources/views/SymfonyProfiler/edit.html.twig b/Resources/views/SymfonyProfiler/edit.html.twig index 40fcea5a..2a528467 100644 --- a/Resources/views/SymfonyProfiler/edit.html.twig +++ b/Resources/views/SymfonyProfiler/edit.html.twig @@ -1,3 +1,3 @@ - + diff --git a/Resources/views/SymfonyProfiler/translation.html.twig b/Resources/views/SymfonyProfiler/translation.html.twig index c307bcd9..517e23e7 100644 --- a/Resources/views/SymfonyProfiler/translation.html.twig +++ b/Resources/views/SymfonyProfiler/translation.html.twig @@ -110,7 +110,7 @@ -
From 0f096c79772e03a461ef898df4af9d79eb1349a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20=C3=85rhof?= Date: Sun, 31 Dec 2017 14:00:39 +0100 Subject: [PATCH 017/234] Fix for symfony 4 templates (#169) * Fix for symfony 4 templates * Corrected fix for the twig templating --- Readme.md | 1 - Resources/views/WebUI/index.html.twig | 2 +- Resources/views/WebUI/show.html.twig | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Readme.md b/Readme.md index 3bc32247..69458c4a 100644 --- a/Readme.md +++ b/Readme.md @@ -74,4 +74,3 @@ _translation_edit_in_place: ## Documentation Read the full documentation at [http://php-translation.readthedocs.io](http://php-translation.readthedocs.io/en/latest/). - diff --git a/Resources/views/WebUI/index.html.twig b/Resources/views/WebUI/index.html.twig index c209975b..b2b4f178 100644 --- a/Resources/views/WebUI/index.html.twig +++ b/Resources/views/WebUI/index.html.twig @@ -1,4 +1,4 @@ -{% extends "TranslationBundle:WebUI:base.html.twig" %} +{% extends "@Translation/WebUI/base.html.twig" %} {% import _self as macro %} {% block body %} diff --git a/Resources/views/WebUI/show.html.twig b/Resources/views/WebUI/show.html.twig index 59bdfc6f..22bbe7f8 100644 --- a/Resources/views/WebUI/show.html.twig +++ b/Resources/views/WebUI/show.html.twig @@ -1,4 +1,4 @@ -{% extends "TranslationBundle:WebUI:base.html.twig" %} +{% extends "@Translation/WebUI/base.html.twig" %} {% import _self as macro %} {% block body %} From 496c550c4062668f7def7e5fb98f0019f4ee0e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20=C3=85rhof?= Date: Sun, 31 Dec 2017 15:55:11 +0100 Subject: [PATCH 018/234] Feature/webui overview (#172) * Sorting message files, so they are similar on all languages * Moved message count from controller to twig, due to it affected the total count, Some more details * Cleaned up the total check --- Controller/WebUIController.php | 4 +-- Resources/views/WebUI/index.html.twig | 36 +++++++++++++++++++++------ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index dc1f9c2a..89d53a31 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -58,9 +58,7 @@ public function indexAction($configName = null) ksort($domains); $catalogueSize[$locale] = 0; foreach ($domains as $domain => $messages) { - $count = count(array_filter($messages, function ($message) { - return '' !== $message; - })); + $count = count($messages); $catalogueSize[$locale] += $count; if (!isset($maxDomainSize[$domain]) || $count > $maxDomainSize[$domain]) { $maxDomainSize[$domain] = $count; diff --git a/Resources/views/WebUI/index.html.twig b/Resources/views/WebUI/index.html.twig index b2b4f178..dcc92ae9 100644 --- a/Resources/views/WebUI/index.html.twig +++ b/Resources/views/WebUI/index.html.twig @@ -8,10 +8,18 @@

{{ localeMap[cataloge.locale] }}

- {% for domain,messages in cataloge.all %} - {% set pg = maxDomainSize[domain] %} - {% if pg > 0 %} - {% set pg = (pg/messages|length)|round(2)*100 %} + {% set totalMessages = 0 %} + {% set translatedMessages = 0 %} + {% for domain,messages in cataloge.all|sort %} + {% set pg = 0 %} + {% set translated = 0 %} + {% for message in messages %} + {% if message != '' %} + {% set translated = translated + 1 %} + {% endif %} + {% endfor %} + {% if translated > 0 %} + {% set pg = (translated/messages|length*100)|round(2) %} {% endif %} + {% set totalMessages = totalMessages + messages|length %} + {% set translatedMessages = translatedMessages + translated %} {% endfor %}
@@ -23,13 +31,27 @@ {{ macro.progress(pg, "") }} - {{ pg }} % + ({{ translated }} / {{ messages|length }}) + {{ pg }} %
- {% set pg = (100*catalogueSize[cataloge.locale]/maxCatalogueSize)|round %} -
Total progress for this language: {{ pg }}%
+ {% if totalMessages == 0 %} + {% set pg = 100 %} + {% elseif translatedMessages > 0 and totalMessages > 0 %} + {% set pg = (translatedMessages/totalMessages*100)|round(2) %} + {% else %} + {% set pg = 0 %} + {% endif %} + +
+ Total progress for this language: + ({{ translatedMessages }} / {{ totalMessages }}) + {{ pg }}% +
{{ macro.progress(pg, "total-progressbar") }}
{% endfor %} From af94b00bea3648eb6745d40357d06af7dbcec990 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 2 Jan 2018 09:31:46 +0100 Subject: [PATCH 019/234] Make sure one can dump configuration (#174) This will fix #152 --- DependencyInjection/TranslationExtension.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/DependencyInjection/TranslationExtension.php b/DependencyInjection/TranslationExtension.php index 608c17a7..c1453eb7 100644 --- a/DependencyInjection/TranslationExtension.php +++ b/DependencyInjection/TranslationExtension.php @@ -247,4 +247,12 @@ private function createChildDefinition($parent) return new DefinitionDecorator($parent); } + + /** + * {@inheritdoc} + */ + public function getConfiguration(array $config, ContainerBuilder $container) + { + return new Configuration($container); + } } From 4698e76f8570b01432b503370d71bfc2ce9d59fd Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 2 Jan 2018 20:17:10 +0100 Subject: [PATCH 020/234] depend on router interface instead of concrete implementation (#144) * depend on router interface instead of concrete implementation * change type hint to UrlGeneratorInterface --- EventListener/EditInPlaceResponseListener.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EventListener/EditInPlaceResponseListener.php b/EventListener/EditInPlaceResponseListener.php index c3a7919c..a2a95caa 100644 --- a/EventListener/EditInPlaceResponseListener.php +++ b/EventListener/EditInPlaceResponseListener.php @@ -13,7 +13,7 @@ use Symfony\Component\Asset\Packages; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; -use Symfony\Component\Routing\Router; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Translation\Bundle\EditInPlace\ActivatorInterface; /** @@ -44,7 +44,7 @@ final class EditInPlaceResponseListener private $activator; /** - * @var Router + * @var UrlGeneratorInterface */ private $router; @@ -65,7 +65,7 @@ final class EditInPlaceResponseListener */ private $showUntranslatable; - public function __construct(ActivatorInterface $activator, Router $router, Packages $packages, $configName = 'default', $showUntranslatable = true) + public function __construct(ActivatorInterface $activator, UrlGeneratorInterface $router, Packages $packages, $configName = 'default', $showUntranslatable = true) { $this->activator = $activator; $this->router = $router; From 305f4b186e95e4ebbaaafc50ba90a9f25dff0e5c Mon Sep 17 00:00:00 2001 From: Remon van de Kamp Date: Sat, 6 Jan 2018 10:41:07 +0100 Subject: [PATCH 021/234] Fall back to locale ID when no name is known for locale (#177) * Fix typo in variable name * Fall back to locale ID when no name is known for locale --- Controller/WebUIController.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index 89d53a31..ed1a11ab 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -251,11 +251,11 @@ private function getMessageFromRequest(Request $request) */ private function getLocale2LanguageMap() { - $configuedLocales = $this->getParameter('php_translation.locales'); + $configuredLocales = $this->getParameter('php_translation.locales'); $names = Intl::getLocaleBundle()->getLocaleNames('en'); $map = []; - foreach ($configuedLocales as $l) { - $map[$l] = $names[$l]; + foreach ($configuredLocales as $l) { + $map[$l] = isset($names[$l]) ? $names[$l] : $l; } return $map; From 05d8cf9b0ee55e7fc26ebe85951ba92313e2253d Mon Sep 17 00:00:00 2001 From: Tom Maaswinkel Date: Thu, 11 Jan 2018 16:10:49 +0100 Subject: [PATCH 022/234] Make activator service public The php_translation.edit_in_place.activator service should be public to make the instructions from http://php-translation.readthedocs.io/en/latest/symfony/edit-in-place.html work --- Resources/config/edit_in_place.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/config/edit_in_place.yml b/Resources/config/edit_in_place.yml index 6d89c949..94c5fb20 100644 --- a/Resources/config/edit_in_place.yml +++ b/Resources/config/edit_in_place.yml @@ -13,6 +13,7 @@ services: php_translation.edit_in_place.activator: class: Translation\Bundle\EditInPlace\Activator arguments: ['@session'] + public: true php_translator.edit_in_place.xtrans_html_translator: class: Translation\Bundle\Translator\EditInPlaceTranslator From d9d040435c2dfe6e2ac471022cb197822baf1e9d Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sun, 14 Jan 2018 21:09:23 +0100 Subject: [PATCH 023/234] Support desc filter (#180) * Adding Desc filter * Make sue we save Desc when importing * Make the class final * Adding visitors to twig filter * Added tests * cs * Addd author docs --- Changelog.md | 10 ++ Resources/config/edit_in_place.yml | 2 +- Resources/config/extractors.yml | 2 +- Resources/config/services.yml | 5 +- Service/Importer.php | 3 + Tests/Unit/Twig/BaseTwigTestCase.php | 35 +++++ .../Twig/DefaultApplyingNodeVisitorTest.php | 26 ++++ .../Fixture/apply_default_value.html.twig | 4 + .../apply_default_value_compiled.html.twig | 4 + .../binary_concat_of_constants.html.twig | 3 + ...ary_concat_of_constants_compiled.html.twig | 1 + .../Twig/Fixture/simple_template.html.twig | 21 +++ .../simple_template_compiled.html.twig | 21 +++ .../Unit/Twig/NormalizingNodeVisitorTest.php | 26 ++++ Tests/Unit/Twig/RemovingNodeVisitorTest.php | 26 ++++ Twig/EditInPlaceExtension.php | 79 +++++++++++ Twig/Node/Transchoice.php | 43 ++++++ Twig/TranslationExtension.php | 93 ++++++++---- Twig/Visitor/DefaultApplyingNodeVisitor.php | 134 ++++++++++++++++++ Twig/{ => Visitor}/DummyTwigVisitor.php | 2 +- Twig/Visitor/NormalizingNodeVisitor.php | 59 ++++++++ Twig/Visitor/RemovingNodeVisitor.php | 71 ++++++++++ 22 files changed, 639 insertions(+), 31 deletions(-) create mode 100644 Tests/Unit/Twig/BaseTwigTestCase.php create mode 100644 Tests/Unit/Twig/DefaultApplyingNodeVisitorTest.php create mode 100644 Tests/Unit/Twig/Fixture/apply_default_value.html.twig create mode 100644 Tests/Unit/Twig/Fixture/apply_default_value_compiled.html.twig create mode 100644 Tests/Unit/Twig/Fixture/binary_concat_of_constants.html.twig create mode 100644 Tests/Unit/Twig/Fixture/binary_concat_of_constants_compiled.html.twig create mode 100644 Tests/Unit/Twig/Fixture/simple_template.html.twig create mode 100644 Tests/Unit/Twig/Fixture/simple_template_compiled.html.twig create mode 100644 Tests/Unit/Twig/NormalizingNodeVisitorTest.php create mode 100644 Tests/Unit/Twig/RemovingNodeVisitorTest.php create mode 100644 Twig/EditInPlaceExtension.php create mode 100644 Twig/Node/Transchoice.php create mode 100644 Twig/Visitor/DefaultApplyingNodeVisitor.php rename Twig/{ => Visitor}/DummyTwigVisitor.php (91%) create mode 100644 Twig/Visitor/NormalizingNodeVisitor.php create mode 100644 Twig/Visitor/RemovingNodeVisitor.php diff --git a/Changelog.md b/Changelog.md index 8a4a8e39..77eea3d6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,16 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" betwee ### Added +- Support for `desc` filter in Twig. + +### Changed + +- Twig extension `TranslationExtension` was renamed to `EditInPlaceExtension` + +## 0.5.0 + +### Added + - Symfony 4 support - New `--cache` option on the `translation:download` allowing to clear the cache automatically if the downloaded translations have changed. - Support for Yandex translator diff --git a/Resources/config/edit_in_place.yml b/Resources/config/edit_in_place.yml index 94c5fb20..23174c11 100644 --- a/Resources/config/edit_in_place.yml +++ b/Resources/config/edit_in_place.yml @@ -24,7 +24,7 @@ services: php_translation.edit_in_place.extension.trans: public: false - class: Translation\Bundle\Twig\TranslationExtension + class: Translation\Bundle\Twig\EditInPlaceExtension arguments: - '@php_translator.edit_in_place.xtrans_html_translator' calls: diff --git a/Resources/config/extractors.yml b/Resources/config/extractors.yml index efb9c280..4edbfa48 100644 --- a/Resources/config/extractors.yml +++ b/Resources/config/extractors.yml @@ -73,7 +73,7 @@ services: public: false php_translation.extractor.twig.visitor.twig: - class: Translation\Bundle\Twig\DummyTwigVisitor + class: Translation\Bundle\Twig\Visitor\DummyTwigVisitor factory: ["@php_translation.extractor.twig.factory", create] tags: - { name: 'php_translation.visitor', type: 'twig' } diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 89bd5b9e..3877bd97 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -56,6 +56,7 @@ services: arguments: [] php_translation.twig_extension: - class: Translation\Extractor\Twig\TranslationExtension + class: Translation\Bundle\Twig\TranslationExtension + arguments: ['@translator', "%kernel.debug%"] tags: - - { name: twig.extension } \ No newline at end of file + - { name: twig.extension } diff --git a/Service/Importer.php b/Service/Importer.php index b8125f0f..1760b77d 100644 --- a/Service/Importer.php +++ b/Service/Importer.php @@ -120,6 +120,9 @@ private function convertSourceLocationsToMessages(MessageCatalogue $catalogue, S $meta = $this->getMetadata($catalogue, $key, $domain); $meta->addCategory('file-source', sprintf('%s:%s', substr($sourceLocation->getPath(), $trimLength), $sourceLocation->getLine())); + if (isset($sourceLocation->getContext()['desc'])) { + $meta->addCategory('desc', $sourceLocation->getContext()['desc']); + } $this->setMetadata($catalogue, $key, $domain, $meta); } } diff --git a/Tests/Unit/Twig/BaseTwigTestCase.php b/Tests/Unit/Twig/BaseTwigTestCase.php new file mode 100644 index 00000000..7ef74766 --- /dev/null +++ b/Tests/Unit/Twig/BaseTwigTestCase.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Tests\Unit\Twig; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Translation\MessageSelector; +use Symfony\Component\Translation\IdentityTranslator; +use Symfony\Bridge\Twig\Extension\TranslationExtension as SymfonyTranslationExtension; +use Translation\Bundle\Twig\TranslationExtension; + +/** + * @author Johannes M. Schmitt + */ +abstract class BaseTwigTestCase extends TestCase +{ + final protected function parse($file, $debug = false) + { + $content = file_get_contents(__DIR__.'/Fixture/'.$file); + + $env = new \Twig_Environment(new \Twig_Loader_Array([])); + $env->addExtension(new SymfonyTranslationExtension($translator = new IdentityTranslator(new MessageSelector()))); + $env->addExtension(new TranslationExtension($translator, $debug)); + + return $env->parse($env->tokenize(new \Twig_Source($content, null)))->getNode('body'); + } +} diff --git a/Tests/Unit/Twig/DefaultApplyingNodeVisitorTest.php b/Tests/Unit/Twig/DefaultApplyingNodeVisitorTest.php new file mode 100644 index 00000000..bee3c488 --- /dev/null +++ b/Tests/Unit/Twig/DefaultApplyingNodeVisitorTest.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Tests\Unit\Twig; + +/** + * @author Johannes M. Schmitt + */ +class DefaultApplyingNodeVisitorTest extends BaseTwigTestCase +{ + public function testApply() + { + $this->assertEquals( + $this->parse('apply_default_value_compiled.html.twig', true), + $this->parse('apply_default_value.html.twig', true) + ); + } +} diff --git a/Tests/Unit/Twig/Fixture/apply_default_value.html.twig b/Tests/Unit/Twig/Fixture/apply_default_value.html.twig new file mode 100644 index 00000000..ccb4d458 --- /dev/null +++ b/Tests/Unit/Twig/Fixture/apply_default_value.html.twig @@ -0,0 +1,4 @@ +{{ "form.label.firstname"|trans|desc("Firstname") }} + +{{ "foo.%bar%"|trans({"%bar%": "baz"})|desc("Foo %bar%") }} + diff --git a/Tests/Unit/Twig/Fixture/apply_default_value_compiled.html.twig b/Tests/Unit/Twig/Fixture/apply_default_value_compiled.html.twig new file mode 100644 index 00000000..254e765d --- /dev/null +++ b/Tests/Unit/Twig/Fixture/apply_default_value_compiled.html.twig @@ -0,0 +1,4 @@ +{{ "form.label.firstname"|trans == "form.label.firstname" ? "Firstname" : "form.label.firstname"|trans }} + +{{ "foo.%bar%"|trans({}) == "foo.%bar%" ? "Foo %bar%"|replace({"%bar%": "baz"}) : "foo.%bar%"|trans({"%bar%": "baz"}) }} + diff --git a/Tests/Unit/Twig/Fixture/binary_concat_of_constants.html.twig b/Tests/Unit/Twig/Fixture/binary_concat_of_constants.html.twig new file mode 100644 index 00000000..99cc553e --- /dev/null +++ b/Tests/Unit/Twig/Fixture/binary_concat_of_constants.html.twig @@ -0,0 +1,3 @@ +{{ "foo" + ~ "bar" + ~ "baz" }} \ No newline at end of file diff --git a/Tests/Unit/Twig/Fixture/binary_concat_of_constants_compiled.html.twig b/Tests/Unit/Twig/Fixture/binary_concat_of_constants_compiled.html.twig new file mode 100644 index 00000000..ed9aff23 --- /dev/null +++ b/Tests/Unit/Twig/Fixture/binary_concat_of_constants_compiled.html.twig @@ -0,0 +1 @@ +{{ "foobarbaz" }} \ No newline at end of file diff --git a/Tests/Unit/Twig/Fixture/simple_template.html.twig b/Tests/Unit/Twig/Fixture/simple_template.html.twig new file mode 100644 index 00000000..2d56ebef --- /dev/null +++ b/Tests/Unit/Twig/Fixture/simple_template.html.twig @@ -0,0 +1,21 @@ +{{ "text.foo"|trans|desc("Foo Bar")|meaning("Some Meaning")}} + +{{ "text.bar"|trans|desc("Foo") }} + +{{ "text.baz"|trans|meaning("Bar") }} + +{{ "text.foo_bar"|trans({}, "foo") }} + +{% trans with {'%name%': 'Johannes'} from "app" %}text.name{% endtrans %} + +{% transchoice count with {'%name%': 'Johannes'} from "app" %}text.apple_choice{% endtranschoice %} + +{{ "foo.bar" | trans }} + +{{ "foo.bar2" | transchoice(5) }} + +{{ "foo.bar3" | trans({'%name%': 'Johannes'}, "app") }} + +{{ "foo.bar4" | transchoice(5, {'%name%': 'Johannes'}, 'app') }} + +{% trans %}text.default_domain{% endtrans %} \ No newline at end of file diff --git a/Tests/Unit/Twig/Fixture/simple_template_compiled.html.twig b/Tests/Unit/Twig/Fixture/simple_template_compiled.html.twig new file mode 100644 index 00000000..4f262627 --- /dev/null +++ b/Tests/Unit/Twig/Fixture/simple_template_compiled.html.twig @@ -0,0 +1,21 @@ +{{ "text.foo"|trans }} + +{{ "text.bar"|trans }} + +{{ "text.baz"|trans }} + +{{ "text.foo_bar"|trans({}, "foo") }} + +{% trans with {'%name%': 'Johannes'} from "app" %}text.name{% endtrans %} + +{% transchoice count with {'%name%': 'Johannes'} from "app" %}text.apple_choice{% endtranschoice %} + +{{ "foo.bar" | trans }} + +{{ "foo.bar2" | transchoice(5) }} + +{{ "foo.bar3" | trans({'%name%': 'Johannes'}, "app") }} + +{{ "foo.bar4" | transchoice(5, {'%name%': 'Johannes'}, 'app') }} + +{% trans %}text.default_domain{% endtrans %} \ No newline at end of file diff --git a/Tests/Unit/Twig/NormalizingNodeVisitorTest.php b/Tests/Unit/Twig/NormalizingNodeVisitorTest.php new file mode 100644 index 00000000..1879b102 --- /dev/null +++ b/Tests/Unit/Twig/NormalizingNodeVisitorTest.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Tests\Unit\Twig; + +/** + * @author Johannes M. Schmitt + */ +class NormalizingNodeVisitorTest extends BaseTwigTestCase +{ + public function testBinaryConcatOfConstants() + { + $this->assertEquals( + $this->parse('binary_concat_of_constants_compiled.html.twig'), + $this->parse('binary_concat_of_constants.html.twig') + ); + } +} diff --git a/Tests/Unit/Twig/RemovingNodeVisitorTest.php b/Tests/Unit/Twig/RemovingNodeVisitorTest.php new file mode 100644 index 00000000..09b92b4d --- /dev/null +++ b/Tests/Unit/Twig/RemovingNodeVisitorTest.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Tests\Unit\Twig; + +/** + * @author Johannes M. Schmitt + */ +class RemovingNodeVisitorTest extends BaseTwigTestCase +{ + public function testRemovalWithSimpleTemplate() + { + $expected = $this->parse('simple_template_compiled.html.twig'); + $actual = $this->parse('simple_template.html.twig'); + + $this->assertEquals($expected, $actual); + } +} diff --git a/Twig/EditInPlaceExtension.php b/Twig/EditInPlaceExtension.php new file mode 100644 index 00000000..14eb9923 --- /dev/null +++ b/Twig/EditInPlaceExtension.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Twig; + +use Symfony\Component\HttpFoundation\RequestStack; +use Translation\Bundle\EditInPlace\ActivatorInterface; + +/** + * Override the `trans` functions `is_safe` option to allow HTML output from the + * translator. This extension is used by for the EditInPlace feature. + * + * @author Damien Alexandre + */ +final class EditInPlaceExtension extends \Symfony\Bridge\Twig\Extension\TranslationExtension +{ + /** + * @var ActivatorInterface + */ + private $activator; + + /** + * @var RequestStack + */ + private $requestStack; + + /** + * {@inheritdoc} + */ + public function getFilters() + { + return [ + new \Twig_SimpleFilter('trans', [$this, 'trans'], ['is_safe_callback' => [$this, 'isSafe']]), + new \Twig_SimpleFilter('transchoice', [$this, 'transchoice'], ['is_safe_callback' => [$this, 'isSafe']]), + ]; + } + + /** + * Escape output if the EditInPlace is disabled. + * + * @return array + */ + public function isSafe($node) + { + return $this->activator->checkRequest($this->requestStack->getMasterRequest()) ? ['html'] : []; + } + + /** + * @param ActivatorInterface $activator + */ + public function setActivator(ActivatorInterface $activator) + { + $this->activator = $activator; + } + + /** + * @param RequestStack $requestStack + */ + public function setRequestStack(RequestStack $requestStack) + { + $this->requestStack = $requestStack; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return self::class; + } +} diff --git a/Twig/Node/Transchoice.php b/Twig/Node/Transchoice.php new file mode 100644 index 00000000..654688f3 --- /dev/null +++ b/Twig/Node/Transchoice.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Twig\Node; + +class Transchoice extends \Twig_Node_Expression +{ + public function __construct(\Twig_Node_Expression_Array $arguments, $lineno) + { + parent::__construct(['arguments' => $arguments], [], $lineno); + } + + public function compile(\Twig_Compiler $compiler) + { + $compiler->raw( + sprintf( + '$this->env->getExtension(\'%s\')->%s(', + 'Translation\Bundle\Twig\TranslationExtension', + 'transchoiceWithDefault' + ) + ); + + $first = true; + foreach ($this->getNode('arguments')->getKeyValuePairs() as $pair) { + if (!$first) { + $compiler->raw(', '); + } + $first = false; + + $compiler->subcompile($pair['value']); + } + + $compiler->raw(')'); + } +} diff --git a/Twig/TranslationExtension.php b/Twig/TranslationExtension.php index 2da44391..02c556a2 100644 --- a/Twig/TranslationExtension.php +++ b/Twig/TranslationExtension.php @@ -11,69 +11,110 @@ namespace Translation\Bundle\Twig; -use Symfony\Component\HttpFoundation\RequestStack; -use Translation\Bundle\EditInPlace\ActivatorInterface; +use Symfony\Component\Translation\TranslatorInterface; +use Translation\Bundle\Twig\Visitor\DefaultApplyingNodeVisitor; +use Translation\Bundle\Twig\Visitor\NormalizingNodeVisitor; +use Translation\Bundle\Twig\Visitor\RemovingNodeVisitor; /** - * Override the `trans` functions `is_safe` option to allow HTML output from the - * translator. This extension is used by for the EditInPlace feature. - * - * @author Damien Alexandre + * @author Johannes M. Schmitt + * @author Tobias Nyholm */ -class TranslationExtension extends \Symfony\Bridge\Twig\Extension\TranslationExtension +final class TranslationExtension extends \Twig_Extension { /** - * @var ActivatorInterface + * @var TranslatorInterface */ - private $activator; + private $translator; /** - * @var RequestStack + * @var bool */ - private $requestStack; + private $debug; + + /** + * @param TranslatorInterface $translator + * @param bool $debug + */ + public function __construct(TranslatorInterface $translator, $debug = false) + { + $this->translator = $translator; + $this->debug = $debug; + } /** - * {@inheritdoc} + * @return array */ public function getFilters() { return [ - new \Twig_SimpleFilter('trans', [$this, 'trans'], ['is_safe_callback' => [$this, 'isSafe']]), - new \Twig_SimpleFilter('transchoice', [$this, 'transchoice'], ['is_safe_callback' => [$this, 'isSafe']]), + new \Twig_SimpleFilter('desc', [$this, 'desc']), + new \Twig_SimpleFilter('meaning', [$this, 'meaning']), ]; } /** - * Escape output if the EditInPlace is disabled. - * * @return array */ - public function isSafe($node) + public function getNodeVisitors() { - return $this->activator->checkRequest($this->requestStack->getMasterRequest()) ? ['html'] : []; + $visitors = [ + new NormalizingNodeVisitor(), + new RemovingNodeVisitor(), + ]; + + if ($this->debug) { + $visitors[] = new DefaultApplyingNodeVisitor(); + } + + return $visitors; } /** - * @param ActivatorInterface $activator + * @param string $message + * @param string $defaultMessage + * @param int $count + * @param array $arguments + * @param null|string $domain + * @param null|string $locale + * + * @return string */ - public function setActivator(ActivatorInterface $activator) + public function transchoiceWithDefault($message, $defaultMessage, $count, array $arguments = [], $domain = null, $locale = null) { - $this->activator = $activator; + if (null === $domain) { + $domain = 'messages'; + } + + if (false === $this->translator->getCatalogue($locale)->defines($message, $domain)) { + return $this->translator->transChoice($defaultMessage, $count, array_merge(['%count%' => $count], $arguments), $domain, $locale); + } + + return $this->translator->transChoice($message, $count, array_merge(['%count%' => $count], $arguments), $domain, $locale); } /** - * @param RequestStack $requestStack + * @param $v + * + * @return mixed */ - public function setRequestStack(RequestStack $requestStack) + public function desc($v) { - $this->requestStack = $requestStack; + return $v; } /** - * {@inheritdoc} + * @param $v + * + * @return mixed */ + public function meaning($v) + { + return $v; + } + public function getName() { - return self::class; + return 'php-translation'; } } diff --git a/Twig/Visitor/DefaultApplyingNodeVisitor.php b/Twig/Visitor/DefaultApplyingNodeVisitor.php new file mode 100644 index 00000000..a6618eaa --- /dev/null +++ b/Twig/Visitor/DefaultApplyingNodeVisitor.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Twig\Visitor; + +use Translation\Bundle\Twig\Node\Transchoice; + +/** + * Applies the value of the "desc" filter if the "trans" filter has no + * translations. + * + * This is only active in your development environment. + * + * @author Johannes M. Schmitt + */ +final class DefaultApplyingNodeVisitor extends \Twig_BaseNodeVisitor +{ + /** + * @var bool + */ + private $enabled = true; + + /** + * @param $bool + */ + public function setEnabled($bool) + { + $this->enabled = (bool) $bool; + } + + /** + * @param \Twig_Node $node + * @param \Twig_Environment $env + * + * @return \Twig_Node + */ + public function doEnterNode(\Twig_Node $node, \Twig_Environment $env) + { + if (!$this->enabled) { + return $node; + } + + if ($node instanceof \Twig_Node_Expression_Filter && 'desc' === $node->getNode('filter')->getAttribute('value')) { + $transNode = $node->getNode('node'); + while ($transNode instanceof \Twig_Node_Expression_Filter + && 'trans' !== $transNode->getNode('filter')->getAttribute('value') + && 'transchoice' !== $transNode->getNode('filter')->getAttribute('value')) { + $transNode = $transNode->getNode('node'); + } + + if (!$transNode instanceof \Twig_Node_Expression_Filter) { + throw new \RuntimeException(sprintf('The "desc" filter must be applied after a "trans", or "transchoice" filter.')); + } + + $wrappingNode = $node->getNode('node'); + $testNode = clone $wrappingNode; + $defaultNode = $node->getNode('arguments')->getNode(0); + + // if the |transchoice filter is used, delegate the call to the TranslationExtension + // so that we can catch a possible exception when the default translation has not yet + // been extracted + if ('transchoice' === $transNode->getNode('filter')->getAttribute('value')) { + $transchoiceArguments = new \Twig_Node_Expression_Array([], $transNode->getTemplateLine()); + $transchoiceArguments->addElement($wrappingNode->getNode('node')); + $transchoiceArguments->addElement($defaultNode); + foreach ($wrappingNode->getNode('arguments') as $arg) { + $transchoiceArguments->addElement($arg); + } + + $transchoiceNode = new Transchoice($transchoiceArguments, $transNode->getTemplateLine()); + $node->setNode('node', $transchoiceNode); + + return $node; + } + + // if the |trans filter has replacements parameters + // (e.g. |trans({'%foo%': 'bar'})) + if ($wrappingNode->getNode('arguments')->hasNode(0)) { + $lineno = $wrappingNode->getTemplateLine(); + + // remove the replacements from the test node + $testNode->setNode('arguments', clone $testNode->getNode('arguments')); + $testNode->getNode('arguments')->setNode(0, new \Twig_Node_Expression_Array([], $lineno)); + + // wrap the default node in a |replace filter + $defaultNode = new \Twig_Node_Expression_Filter( + clone $node->getNode('arguments')->getNode(0), + new \Twig_Node_Expression_Constant('replace', $lineno), + new \Twig_Node([ + clone $wrappingNode->getNode('arguments')->getNode(0), + ]), + $lineno + ); + } + + $condition = new \Twig_Node_Expression_Conditional( + new \Twig_Node_Expression_Binary_Equal($testNode, $transNode->getNode('node'), $wrappingNode->getTemplateLine()), + $defaultNode, + clone $wrappingNode, + $wrappingNode->getTemplateLine() + ); + $node->setNode('node', $condition); + } + + return $node; + } + + /** + * @param \Twig_Node $node + * @param \Twig_Environment $env + * + * @return \Twig_Node + */ + public function doLeaveNode(\Twig_Node $node, \Twig_Environment $env) + { + return $node; + } + + /** + * @return int + */ + public function getPriority() + { + return -2; + } +} diff --git a/Twig/DummyTwigVisitor.php b/Twig/Visitor/DummyTwigVisitor.php similarity index 91% rename from Twig/DummyTwigVisitor.php rename to Twig/Visitor/DummyTwigVisitor.php index 467ca2be..6e983e28 100644 --- a/Twig/DummyTwigVisitor.php +++ b/Twig/Visitor/DummyTwigVisitor.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Translation\Bundle\Twig; +namespace Translation\Bundle\Twig\Visitor; /** * This dummy extractor is used to be compatible with both Twig1 and Twig2. It is only used in dependency injection diff --git a/Twig/Visitor/NormalizingNodeVisitor.php b/Twig/Visitor/NormalizingNodeVisitor.php new file mode 100644 index 00000000..9b600bd4 --- /dev/null +++ b/Twig/Visitor/NormalizingNodeVisitor.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Twig\Visitor; + +/** + * Performs equivalence transformations on the AST to ensure that + * subsequent visitors do not need to be aware of different syntaxes. + * + * E.g. "foo" ~ "bar" ~ "baz" would become "foobarbaz" + * + * @author Johannes M. Schmitt + */ +final class NormalizingNodeVisitor extends \Twig_BaseNodeVisitor +{ + /** + * @param \Twig_Node $node + * @param \Twig_Environment $env + * + * @return \Twig_Node + */ + protected function doEnterNode(\Twig_Node $node, \Twig_Environment $env) + { + return $node; + } + + /** + * @param \Twig_Node $node + * @param \Twig_Environment $env + * + * @return \Twig_Node_Expression_Constant|\Twig_Node + */ + protected function doLeaveNode(\Twig_Node $node, \Twig_Environment $env) + { + if ($node instanceof \Twig_Node_Expression_Binary_Concat + && ($left = $node->getNode('left')) instanceof \Twig_Node_Expression_Constant + && ($right = $node->getNode('right')) instanceof \Twig_Node_Expression_Constant) { + return new \Twig_Node_Expression_Constant($left->getAttribute('value').$right->getAttribute('value'), $left->getTemplateLine()); + } + + return $node; + } + + /** + * @return int + */ + public function getPriority() + { + return -3; + } +} diff --git a/Twig/Visitor/RemovingNodeVisitor.php b/Twig/Visitor/RemovingNodeVisitor.php new file mode 100644 index 00000000..ff69a3f3 --- /dev/null +++ b/Twig/Visitor/RemovingNodeVisitor.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Twig\Visitor; + +/** + * Removes translation metadata filters from the AST. + * + * @author Johannes M. Schmitt + */ +final class RemovingNodeVisitor extends \Twig_BaseNodeVisitor +{ + /** + * @var bool + */ + private $enabled = true; + + /** + * @param $bool + */ + public function setEnabled($bool) + { + $this->enabled = (bool) $bool; + } + + /** + * @param \Twig_Node $node + * @param \Twig_Environment $env + * + * @return \Twig_Node + */ + protected function doEnterNode(\Twig_Node $node, \Twig_Environment $env) + { + if ($this->enabled && $node instanceof \Twig_Node_Expression_Filter) { + $name = $node->getNode('filter')->getAttribute('value'); + + if ('desc' === $name || 'meaning' === $name) { + return $this->enterNode($node->getNode('node'), $env); + } + } + + return $node; + } + + /** + * @param \Twig_Node $node + * @param \Twig_Environment $env + * + * @return \Twig_Node + */ + protected function doLeaveNode(\Twig_Node $node, \Twig_Environment $env) + { + return $node; + } + + /** + * @return int + */ + public function getPriority() + { + return -1; + } +} From a772cdb989c1a7aac494997a7b5fec1eef51fec2 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Wed, 17 Jan 2018 13:24:40 +0100 Subject: [PATCH 024/234] Prepare for 0.6.0 (#184) * Prepare for 0.6.0 * Update composer.json --- Changelog.md | 19 +++++++++++++++++++ composer.json | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 77eea3d6..4f85ac2f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,25 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.6.0 + +### Added + +- Support for Symfony 4 +- Support for `desc` Twig filter +- Support for extract/update only for one bundle + +### Fixed + +- Dump configuration reference +- Improved statistics on WebUI + +### Changed + +- Commands are registered as services +- `EditInPlaceResponseListener::__construct` uses `UrlGeneratorInterface` instead of the concreate class `Router` +- The `php_translation.edit_in_place.activator` service is public + ## 0.5.0 ### Added diff --git a/composer.json b/composer.json index 25d39b2d..970de800 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.4-dev" + "dev-master": "0.7-dev" } } } From 7c58aaa845e527857011702192ef55e4492660ea Mon Sep 17 00:00:00 2001 From: Jop Peters Date: Thu, 18 Jan 2018 20:08:15 +0100 Subject: [PATCH 025/234] Fix fetching config for symfony4 (#187) --- Command/DownloadCommand.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Command/DownloadCommand.php b/Command/DownloadCommand.php index 53bfa0d8..3e3ced85 100644 --- a/Command/DownloadCommand.php +++ b/Command/DownloadCommand.php @@ -45,8 +45,10 @@ protected function execute(InputInterface $input, OutputInterface $output) /** @var StorageService $storage */ $storage = $container->get('php_translation.storage.'.$configName); + /** @var Configuration $configuration */ - $configuration = $this->getContainer()->get('php_translation.configuration.'.$configName); + $configuration = $container->get('php_translation.configuration_manager') + ->getConfiguration($input->getArgument('configuration')); $this->configureBundleDirs($input, $configuration); if ($input->getOption('cache')) { From 405235470988763cf3e5b34d5a85c44c5212e7f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20=C3=85rhof?= Date: Sun, 4 Feb 2018 12:01:13 +0100 Subject: [PATCH 026/234] Feature/command obsolete (#173) * Setting the php_translation.storage.default to public * Updated obsolete command, with some more debug if needed, and dont ask if no messages are obsolete * Style fix * minor fixes * cs --- Command/DeleteObsoleteCommand.php | 32 +++++++++++++++++--- DependencyInjection/TranslationExtension.php | 2 +- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/Command/DeleteObsoleteCommand.php b/Command/DeleteObsoleteCommand.php index 8ded1ee9..215cb62e 100644 --- a/Command/DeleteObsoleteCommand.php +++ b/Command/DeleteObsoleteCommand.php @@ -54,17 +54,41 @@ protected function execute(InputInterface $input, OutputInterface $output) $storage = $container->get('php_translation.storage.'.$configName); $messages = $catalogueManager->findMessages(['locale' => $inputLocale, 'isObsolete' => true]); + $messageCount = count($messages); + if (0 === $messageCount) { + $output->writeln('No messages are obsolete'); + + return; + } + $helper = $this->getHelper('question'); - $question = new ConfirmationQuestion(sprintf('You are about to remove %d translations. Do you wish to continue?', count($messages)), false); + $question = new ConfirmationQuestion(sprintf('You are about to remove %d translations. Do you wish to continue? (y/N) ', $messageCount), false); if (!$helper->ask($input, $output, $question)) { return; } - $progress = new ProgressBar($output, count($messages)); + $progress = null; + if (OutputInterface::VERBOSITY_NORMAL === $output->getVerbosity() && OutputInterface::VERBOSITY_QUIET !== $output->getVerbosity()) { + $progress = new ProgressBar($output, $messageCount); + } foreach ($messages as $message) { $storage->delete($message->getLocale(), $message->getDomain(), $message->getKey()); - $progress->advance(); + if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) { + $output->writeln(sprintf( + 'Deleted obsolete message "%s" from domain "%s" and locale "%s"', + $message->getKey(), + $message->getDomain(), + $message->getLocale() + )); + } + + if ($progress) { + $progress->advance(); + } + } + + if ($progress) { + $progress->finish(); } - $progress->finish(); } } diff --git a/DependencyInjection/TranslationExtension.php b/DependencyInjection/TranslationExtension.php index c1453eb7..4f9f0ac6 100644 --- a/DependencyInjection/TranslationExtension.php +++ b/DependencyInjection/TranslationExtension.php @@ -142,7 +142,7 @@ private function handleConfigNode(ContainerBuilder $container, array $config) // Create some aliases for the default storage $container->setAlias('php_translation.storage', new Alias('php_translation.storage.'.$first, true)); if ('default' !== $first) { - $container->setAlias('php_translation.storage.default', 'php_translation.storage.'.$first); + $container->setAlias('php_translation.storage.default', new Alias('php_translation.storage.'.$first, true)); } } } From b49d4233e868609f4b3cce93958647049133397b Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sun, 4 Feb 2018 12:58:39 +0100 Subject: [PATCH 027/234] Code clean up (#138) * Code clean up * minor --- Model/Metadata.php | 2 +- Tests/Unit/Catalogue/CatalogueManagerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Model/Metadata.php b/Model/Metadata.php index d78f0624..6cd0d4b2 100644 --- a/Model/Metadata.php +++ b/Model/Metadata.php @@ -139,7 +139,7 @@ public function toArray() public function getAllInCategory($category) { $data = []; - foreach ($this->notes as &$note) { + foreach ($this->notes as $note) { if (!isset($note['category'])) { continue; } diff --git a/Tests/Unit/Catalogue/CatalogueManagerTest.php b/Tests/Unit/Catalogue/CatalogueManagerTest.php index ac71dde1..cbd62e91 100644 --- a/Tests/Unit/Catalogue/CatalogueManagerTest.php +++ b/Tests/Unit/Catalogue/CatalogueManagerTest.php @@ -25,7 +25,7 @@ public function testGetMessages() { $manager = new CatalogueManager(); $catA = new MessageCatalogue('en', ['messages' => ['a' => 'aTrans', 'b' => 'bTrans']]); - $catB = new MessageCatalogue('fr', ['messages' => ['a' => 'aTransFr', 'c' => 'cTransFr']]); + $catB = new MessageCatalogue('fr', ['messages' => ['a' => 'aTransFr', 'c' => 'cTransFr', 'd' => 'dTransFr']]); $manager->load([$catA, $catB]); $messages = $manager->getMessages('en', 'messages'); From 41ff01c7fd160e132607df2f2ce2c9caed4c0297 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sun, 4 Feb 2018 12:58:52 +0100 Subject: [PATCH 028/234] Show deprecation notices (#164) * Show deprecation notices * Disable backups --- .travis.yml | 1 - Resources/config/services.yml | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f75590e7..39999d72 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,6 @@ env: global: - TEST_COMMAND="composer test" - SYMFONY_PHPUNIT_VERSION="6.3" - - SYMFONY_DEPRECATIONS_HELPER="weak" # Temporary, To be removed matrix: fast_finish: true diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 3877bd97..732e93cf 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -49,6 +49,8 @@ services: class: Translation\SymfonyStorage\Dumper\XliffDumper tags: - { name: translation.dumper, alias: xlf, legacy-alias: xliff } + calls: + - [setBackup, [false]] php_translation.catalogue_counter: public: true From 356e08336d1726f340f692e41051c1cbd0c3314e Mon Sep 17 00:00:00 2001 From: Mathieu Santostefano Date: Sat, 10 Feb 2018 16:55:13 +0100 Subject: [PATCH 029/234] Update add missing message button label + style (#196) --- Resources/views/SymfonyProfiler/translation.html.twig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Resources/views/SymfonyProfiler/translation.html.twig b/Resources/views/SymfonyProfiler/translation.html.twig index 1177b7a4..23d040f4 100644 --- a/Resources/views/SymfonyProfiler/translation.html.twig +++ b/Resources/views/SymfonyProfiler/translation.html.twig @@ -113,7 +113,7 @@

@@ -171,9 +171,9 @@
{% spaceless %} - Edit + Edit | - Sync + Sync {% endspaceless %}
` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n font-size: $font-size-base;\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Suppress the focus outline on elements that cannot be accessed via keyboard.\n// This prevents an unwanted focus outline from appearing around elements that\n// might still respond to pointer events.\n//\n// Credit: https://github.com/suitcss/base\n[tabindex=\"-1\"]:focus {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n// stylelint-enable selector-list-comma-newline-after\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Remove the bottom border in Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Duplicate behavior to the data-* attribute for our tooltip plugin\n\nabbr[title],\nabbr[data-original-title] { // 4\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 1\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic; // Add the correct font style in Android 4.3-\n}\n\n// stylelint-disable font-weight-notation\nb,\nstrong {\n font-weight: bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n// stylelint-enable font-weight-notation\n\nsmall {\n font-size: 80%; // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n -webkit-text-decoration-skip: objects; // Remove gaps in links underline in iOS 8+ and Safari 8+.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n font-size: 1em; // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // We have @viewport set which causes scrollbars to overlap content in IE11 and Edge, so\n // we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg:not(:root) {\n overflow: hidden; // Hide the overflow in IE\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `

` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\nhtml [type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n // Remove the default appearance of temporal inputs to avoid a Mobile Safari\n // bug where setting a custom line-height prevents text from being vertically\n // centered within the input.\n // See https://bugs.webkit.org/show_bug.cgi?id=139848\n // and https://github.com/twbs/bootstrap/issues/11266\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `
`s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding and cancel buttons in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-cancel-button,\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n cursor: pointer;\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n","/*!\n * Bootstrap v4.1.1 (https://getbootstrap.com/)\n * Copyright 2011-2018 The Bootstrap Authors\n * Copyright 2011-2018 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n:root {\n --blue: #007bff;\n --indigo: #6610f2;\n --purple: #6f42c1;\n --pink: #e83e8c;\n --red: #dc3545;\n --orange: #fd7e14;\n --yellow: #ffc107;\n --green: #28a745;\n --teal: #20c997;\n --cyan: #17a2b8;\n --white: #fff;\n --gray: #6c757d;\n --gray-dark: #343a40;\n --primary: #007bff;\n --secondary: #6c757d;\n --success: #28a745;\n --info: #17a2b8;\n --warning: #ffc107;\n --danger: #dc3545;\n --light: #f8f9fa;\n --dark: #343a40;\n --breakpoint-xs: 0;\n --breakpoint-sm: 576px;\n --breakpoint-md: 768px;\n --breakpoint-lg: 992px;\n --breakpoint-xl: 1200px;\n --font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -ms-text-size-adjust: 100%;\n -ms-overflow-style: scrollbar;\n -webkit-tap-highlight-color: transparent;\n}\n\n@-ms-viewport {\n width: device-width;\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n -webkit-text-decoration-skip: objects;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):focus {\n outline: 0;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n -ms-overflow-style: scrollbar;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nbutton,\nhtml [type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-cancel-button,\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: 0.5rem;\n font-family: inherit;\n font-weight: 500;\n line-height: 1.2;\n color: inherit;\n}\n\nh1, .h1 {\n font-size: 2.5rem;\n}\n\nh2, .h2 {\n font-size: 2rem;\n}\n\nh3, .h3 {\n font-size: 1.75rem;\n}\n\nh4, .h4 {\n font-size: 1.5rem;\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: 6rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-2 {\n font-size: 5.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-3 {\n font-size: 4.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-4 {\n font-size: 3.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\nhr {\n margin-top: 1rem;\n margin-bottom: 1rem;\n border: 0;\n border-top: 1px solid rgba(0, 0, 0, 0.1);\n}\n\nsmall,\n.small {\n font-size: 80%;\n font-weight: 400;\n}\n\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n\n.blockquote-footer {\n display: block;\n font-size: 80%;\n color: #6c757d;\n}\n\n.blockquote-footer::before {\n content: \"\\2014 \\00A0\";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: #fff;\n border: 1px solid #dee2e6;\n border-radius: 0.25rem;\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 90%;\n color: #6c757d;\n}\n\ncode {\n font-size: 87.5%;\n color: #e83e8c;\n word-break: break-word;\n}\n\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.2rem 0.4rem;\n font-size: 87.5%;\n color: #fff;\n background-color: #212529;\n border-radius: 0.2rem;\n}\n\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n}\n\npre {\n display: block;\n font-size: 87.5%;\n color: #212529;\n}\n\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n\n.container {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container {\n max-width: 1140px;\n }\n}\n\n.container-fluid {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n.row {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n min-height: 1px;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.col-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: none;\n}\n\n.col-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n -ms-flex-order: -1;\n order: -1;\n}\n\n.order-last {\n -ms-flex-order: 13;\n order: 13;\n}\n\n.order-0 {\n -ms-flex-order: 0;\n order: 0;\n}\n\n.order-1 {\n -ms-flex-order: 1;\n order: 1;\n}\n\n.order-2 {\n -ms-flex-order: 2;\n order: 2;\n}\n\n.order-3 {\n -ms-flex-order: 3;\n order: 3;\n}\n\n.order-4 {\n -ms-flex-order: 4;\n order: 4;\n}\n\n.order-5 {\n -ms-flex-order: 5;\n order: 5;\n}\n\n.order-6 {\n -ms-flex-order: 6;\n order: 6;\n}\n\n.order-7 {\n -ms-flex-order: 7;\n order: 7;\n}\n\n.order-8 {\n -ms-flex-order: 8;\n order: 8;\n}\n\n.order-9 {\n -ms-flex-order: 9;\n order: 9;\n}\n\n.order-10 {\n -ms-flex-order: 10;\n order: 10;\n}\n\n.order-11 {\n -ms-flex-order: 11;\n order: 11;\n}\n\n.order-12 {\n -ms-flex-order: 12;\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-sm-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: none;\n }\n .col-sm-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-sm-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-sm-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-sm-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-sm-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-sm-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-sm-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-sm-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-sm-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-sm-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-sm-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-sm-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-sm-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-sm-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-sm-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-md-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: none;\n }\n .col-md-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-md-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-md-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-md-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-md-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-md-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-md-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-md-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-md-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-md-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-md-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-md-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-md-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-md-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-md-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-lg-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: none;\n }\n .col-lg-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-lg-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-lg-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-lg-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-lg-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-lg-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-lg-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-lg-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-lg-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-lg-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-lg-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-lg-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-lg-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-lg-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-lg-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-xl-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: none;\n }\n .col-xl-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-xl-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-xl-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-xl-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-xl-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-xl-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-xl-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-xl-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-xl-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-xl-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-xl-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-xl-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-xl-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-xl-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-xl-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 1rem;\n background-color: transparent;\n}\n\n.table th,\n.table td {\n padding: 0.75rem;\n vertical-align: top;\n border-top: 1px solid #dee2e6;\n}\n\n.table thead th {\n vertical-align: bottom;\n border-bottom: 2px solid #dee2e6;\n}\n\n.table tbody + tbody {\n border-top: 2px solid #dee2e6;\n}\n\n.table .table {\n background-color: #fff;\n}\n\n.table-sm th,\n.table-sm td {\n padding: 0.3rem;\n}\n\n.table-bordered {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered th,\n.table-bordered td {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered thead th,\n.table-bordered thead td {\n border-bottom-width: 2px;\n}\n\n.table-borderless th,\n.table-borderless td,\n.table-borderless thead th,\n.table-borderless tbody + tbody {\n border: 0;\n}\n\n.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n.table-hover tbody tr:hover {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-primary,\n.table-primary > th,\n.table-primary > td {\n background-color: #b8daff;\n}\n\n.table-hover .table-primary:hover {\n background-color: #9fcdff;\n}\n\n.table-hover .table-primary:hover > td,\n.table-hover .table-primary:hover > th {\n background-color: #9fcdff;\n}\n\n.table-secondary,\n.table-secondary > th,\n.table-secondary > td {\n background-color: #d6d8db;\n}\n\n.table-hover .table-secondary:hover {\n background-color: #c8cbcf;\n}\n\n.table-hover .table-secondary:hover > td,\n.table-hover .table-secondary:hover > th {\n background-color: #c8cbcf;\n}\n\n.table-success,\n.table-success > th,\n.table-success > td {\n background-color: #c3e6cb;\n}\n\n.table-hover .table-success:hover {\n background-color: #b1dfbb;\n}\n\n.table-hover .table-success:hover > td,\n.table-hover .table-success:hover > th {\n background-color: #b1dfbb;\n}\n\n.table-info,\n.table-info > th,\n.table-info > td {\n background-color: #bee5eb;\n}\n\n.table-hover .table-info:hover {\n background-color: #abdde5;\n}\n\n.table-hover .table-info:hover > td,\n.table-hover .table-info:hover > th {\n background-color: #abdde5;\n}\n\n.table-warning,\n.table-warning > th,\n.table-warning > td {\n background-color: #ffeeba;\n}\n\n.table-hover .table-warning:hover {\n background-color: #ffe8a1;\n}\n\n.table-hover .table-warning:hover > td,\n.table-hover .table-warning:hover > th {\n background-color: #ffe8a1;\n}\n\n.table-danger,\n.table-danger > th,\n.table-danger > td {\n background-color: #f5c6cb;\n}\n\n.table-hover .table-danger:hover {\n background-color: #f1b0b7;\n}\n\n.table-hover .table-danger:hover > td,\n.table-hover .table-danger:hover > th {\n background-color: #f1b0b7;\n}\n\n.table-light,\n.table-light > th,\n.table-light > td {\n background-color: #fdfdfe;\n}\n\n.table-hover .table-light:hover {\n background-color: #ececf6;\n}\n\n.table-hover .table-light:hover > td,\n.table-hover .table-light:hover > th {\n background-color: #ececf6;\n}\n\n.table-dark,\n.table-dark > th,\n.table-dark > td {\n background-color: #c6c8ca;\n}\n\n.table-hover .table-dark:hover {\n background-color: #b9bbbe;\n}\n\n.table-hover .table-dark:hover > td,\n.table-hover .table-dark:hover > th {\n background-color: #b9bbbe;\n}\n\n.table-active,\n.table-active > th,\n.table-active > td {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover > td,\n.table-hover .table-active:hover > th {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table .thead-dark th {\n color: #fff;\n background-color: #212529;\n border-color: #32383e;\n}\n\n.table .thead-light th {\n color: #495057;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.table-dark {\n color: #fff;\n background-color: #212529;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th {\n border-color: #32383e;\n}\n\n.table-dark.table-bordered {\n border: 0;\n}\n\n.table-dark.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(255, 255, 255, 0.05);\n}\n\n.table-dark.table-hover tbody tr:hover {\n background-color: rgba(255, 255, 255, 0.075);\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-sm > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 767.98px) {\n .table-responsive-md {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-md > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-lg > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-xl > .table-bordered {\n border: 0;\n }\n}\n\n.table-responsive {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n}\n\n.table-responsive > .table-bordered {\n border: 0;\n}\n\n.form-control {\n display: block;\n width: 100%;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n\n.form-control:focus {\n color: #495057;\n background-color: #fff;\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.form-control::-webkit-input-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control::-moz-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control:-ms-input-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control::-ms-input-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control::placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control:disabled, .form-control[readonly] {\n background-color: #e9ecef;\n opacity: 1;\n}\n\nselect.form-control:not([size]):not([multiple]) {\n height: calc(2.25rem + 2px);\n}\n\nselect.form-control:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + 1px);\n padding-bottom: calc(0.375rem + 1px);\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + 1px);\n padding-bottom: calc(0.5rem + 1px);\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + 1px);\n padding-bottom: calc(0.25rem + 1px);\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding-top: 0.375rem;\n padding-bottom: 0.375rem;\n margin-bottom: 0;\n line-height: 1.5;\n color: #212529;\n background-color: transparent;\n border: solid transparent;\n border-width: 1px 0;\n}\n\n.form-control-plaintext.form-control-sm, .input-group-sm > .form-control-plaintext.form-control,\n.input-group-sm > .input-group-prepend > .form-control-plaintext.input-group-text,\n.input-group-sm > .input-group-append > .form-control-plaintext.input-group-text,\n.input-group-sm > .input-group-prepend > .form-control-plaintext.btn,\n.input-group-sm > .input-group-append > .form-control-plaintext.btn, .form-control-plaintext.form-control-lg, .input-group-lg > .form-control-plaintext.form-control,\n.input-group-lg > .input-group-prepend > .form-control-plaintext.input-group-text,\n.input-group-lg > .input-group-append > .form-control-plaintext.input-group-text,\n.input-group-lg > .input-group-prepend > .form-control-plaintext.btn,\n.input-group-lg > .input-group-append > .form-control-plaintext.btn {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm, .input-group-sm > .form-control,\n.input-group-sm > .input-group-prepend > .input-group-text,\n.input-group-sm > .input-group-append > .input-group-text,\n.input-group-sm > .input-group-prepend > .btn,\n.input-group-sm > .input-group-append > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\nselect.form-control-sm:not([size]):not([multiple]), .input-group-sm > select.form-control:not([size]):not([multiple]),\n.input-group-sm > .input-group-prepend > select.input-group-text:not([size]):not([multiple]),\n.input-group-sm > .input-group-append > select.input-group-text:not([size]):not([multiple]),\n.input-group-sm > .input-group-prepend > select.btn:not([size]):not([multiple]),\n.input-group-sm > .input-group-append > select.btn:not([size]):not([multiple]) {\n height: calc(1.8125rem + 2px);\n}\n\n.form-control-lg, .input-group-lg > .form-control,\n.input-group-lg > .input-group-prepend > .input-group-text,\n.input-group-lg > .input-group-append > .input-group-text,\n.input-group-lg > .input-group-prepend > .btn,\n.input-group-lg > .input-group-append > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\nselect.form-control-lg:not([size]):not([multiple]), .input-group-lg > select.form-control:not([size]):not([multiple]),\n.input-group-lg > .input-group-prepend > select.input-group-text:not([size]):not([multiple]),\n.input-group-lg > .input-group-append > select.input-group-text:not([size]):not([multiple]),\n.input-group-lg > .input-group-prepend > select.btn:not([size]):not([multiple]),\n.input-group-lg > .input-group-append > select.btn:not([size]):not([multiple]) {\n height: calc(2.875rem + 2px);\n}\n\n.form-group {\n margin-bottom: 1rem;\n}\n\n.form-text {\n display: block;\n margin-top: 0.25rem;\n}\n\n.form-row {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n margin-right: -5px;\n margin-left: -5px;\n}\n\n.form-row > .col,\n.form-row > [class*=\"col-\"] {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.form-check {\n position: relative;\n display: block;\n padding-left: 1.25rem;\n}\n\n.form-check-input {\n position: absolute;\n margin-top: 0.3rem;\n margin-left: -1.25rem;\n}\n\n.form-check-input:disabled ~ .form-check-label {\n color: #6c757d;\n}\n\n.form-check-label {\n margin-bottom: 0;\n}\n\n.form-check-inline {\n display: -ms-inline-flexbox;\n display: inline-flex;\n -ms-flex-align: center;\n align-items: center;\n padding-left: 0;\n margin-right: 0.75rem;\n}\n\n.form-check-inline .form-check-input {\n position: static;\n margin-top: 0;\n margin-right: 0.3125rem;\n margin-left: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #28a745;\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: .5rem;\n margin-top: .1rem;\n font-size: .875rem;\n line-height: 1;\n color: #fff;\n background-color: rgba(40, 167, 69, 0.8);\n border-radius: .2rem;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid, .was-validated\n.custom-select:valid,\n.custom-select.is-valid {\n border-color: #28a745;\n}\n\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus, .was-validated\n.custom-select:valid:focus,\n.custom-select.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .form-control:valid ~ .valid-feedback,\n.was-validated .form-control:valid ~ .valid-tooltip, .form-control.is-valid ~ .valid-feedback,\n.form-control.is-valid ~ .valid-tooltip, .was-validated\n.custom-select:valid ~ .valid-feedback,\n.was-validated\n.custom-select:valid ~ .valid-tooltip,\n.custom-select.is-valid ~ .valid-feedback,\n.custom-select.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control-file:valid ~ .valid-feedback,\n.was-validated .form-control-file:valid ~ .valid-tooltip, .form-control-file.is-valid ~ .valid-feedback,\n.form-control-file.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: #28a745;\n}\n\n.was-validated .form-check-input:valid ~ .valid-feedback,\n.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback,\n.form-check-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label {\n color: #28a745;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before {\n background-color: #71dd8a;\n}\n\n.was-validated .custom-control-input:valid ~ .valid-feedback,\n.was-validated .custom-control-input:valid ~ .valid-tooltip, .custom-control-input.is-valid ~ .valid-feedback,\n.custom-control-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before {\n background-color: #34ce57;\n}\n\n.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid ~ .custom-file-label::before, .custom-file-input.is-valid ~ .custom-file-label::before {\n border-color: inherit;\n}\n\n.was-validated .custom-file-input:valid ~ .valid-feedback,\n.was-validated .custom-file-input:valid ~ .valid-tooltip, .custom-file-input.is-valid ~ .valid-feedback,\n.custom-file-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #dc3545;\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: .5rem;\n margin-top: .1rem;\n font-size: .875rem;\n line-height: 1;\n color: #fff;\n background-color: rgba(220, 53, 69, 0.8);\n border-radius: .2rem;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid, .was-validated\n.custom-select:invalid,\n.custom-select.is-invalid {\n border-color: #dc3545;\n}\n\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus, .was-validated\n.custom-select:invalid:focus,\n.custom-select.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .form-control:invalid ~ .invalid-feedback,\n.was-validated .form-control:invalid ~ .invalid-tooltip, .form-control.is-invalid ~ .invalid-feedback,\n.form-control.is-invalid ~ .invalid-tooltip, .was-validated\n.custom-select:invalid ~ .invalid-feedback,\n.was-validated\n.custom-select:invalid ~ .invalid-tooltip,\n.custom-select.is-invalid ~ .invalid-feedback,\n.custom-select.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control-file:invalid ~ .invalid-feedback,\n.was-validated .form-control-file:invalid ~ .invalid-tooltip, .form-control-file.is-invalid ~ .invalid-feedback,\n.form-control-file.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: #dc3545;\n}\n\n.was-validated .form-check-input:invalid ~ .invalid-feedback,\n.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback,\n.form-check-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label {\n color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before {\n background-color: #efa2a9;\n}\n\n.was-validated .custom-control-input:invalid ~ .invalid-feedback,\n.was-validated .custom-control-input:invalid ~ .invalid-tooltip, .custom-control-input.is-invalid ~ .invalid-feedback,\n.custom-control-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before {\n background-color: #e4606d;\n}\n\n.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid ~ .custom-file-label::before, .custom-file-input.is-invalid ~ .custom-file-label::before {\n border-color: inherit;\n}\n\n.was-validated .custom-file-input:invalid ~ .invalid-feedback,\n.was-validated .custom-file-input:invalid ~ .invalid-tooltip, .custom-file-input.is-invalid ~ .invalid-feedback,\n.custom-file-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.form-inline {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n -ms-flex-align: center;\n align-items: center;\n}\n\n.form-inline .form-check {\n width: 100%;\n}\n\n@media (min-width: 576px) {\n .form-inline label {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n margin-bottom: 0;\n }\n .form-inline .form-group {\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n -ms-flex-align: center;\n align-items: center;\n margin-bottom: 0;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-plaintext {\n display: inline-block;\n }\n .form-inline .input-group,\n .form-inline .custom-select {\n width: auto;\n }\n .form-inline .form-check {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n width: auto;\n padding-left: 0;\n }\n .form-inline .form-check-input {\n position: relative;\n margin-top: 0;\n margin-right: 0.25rem;\n margin-left: 0;\n }\n .form-inline .custom-control {\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n }\n .form-inline .custom-control-label {\n margin-bottom: 0;\n }\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n border: 1px solid transparent;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n line-height: 1.5;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n\n.btn:hover, .btn:focus {\n text-decoration: none;\n}\n\n.btn:focus, .btn.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.btn.disabled, .btn:disabled {\n opacity: 0.65;\n}\n\n.btn:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\n.btn:not(:disabled):not(.disabled):active, .btn:not(:disabled):not(.disabled).active {\n background-image: none;\n}\n\na.btn.disabled,\nfieldset:disabled a.btn {\n pointer-events: none;\n}\n\n.btn-primary {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:hover {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n}\n\n.btn-primary:focus, .btn-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-primary.disabled, .btn-primary:disabled {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active,\n.show > .btn-primary.dropdown-toggle {\n color: #fff;\n background-color: #0062cc;\n border-color: #005cbf;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-secondary {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:hover {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n}\n\n.btn-secondary:focus, .btn-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-secondary.disabled, .btn-secondary:disabled {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-secondary.dropdown-toggle {\n color: #fff;\n background-color: #545b62;\n border-color: #4e555b;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-success {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:hover {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n}\n\n.btn-success:focus, .btn-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-success.disabled, .btn-success:disabled {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active,\n.show > .btn-success.dropdown-toggle {\n color: #fff;\n background-color: #1e7e34;\n border-color: #1c7430;\n}\n\n.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-info {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:hover {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n}\n\n.btn-info:focus, .btn-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-info.disabled, .btn-info:disabled {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,\n.show > .btn-info.dropdown-toggle {\n color: #fff;\n background-color: #117a8b;\n border-color: #10707f;\n}\n\n.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-warning {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:hover {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n}\n\n.btn-warning:focus, .btn-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-warning.disabled, .btn-warning:disabled {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active,\n.show > .btn-warning.dropdown-toggle {\n color: #212529;\n background-color: #d39e00;\n border-color: #c69500;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-danger {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:hover {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n}\n\n.btn-danger:focus, .btn-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-danger.disabled, .btn-danger:disabled {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active,\n.show > .btn-danger.dropdown-toggle {\n color: #fff;\n background-color: #bd2130;\n border-color: #b21f2d;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-light {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:hover {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n}\n\n.btn-light:focus, .btn-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-light.disabled, .btn-light:disabled {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active,\n.show > .btn-light.dropdown-toggle {\n color: #212529;\n background-color: #dae0e5;\n border-color: #d3d9df;\n}\n\n.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-dark {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:hover {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n}\n\n.btn-dark:focus, .btn-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-dark.disabled, .btn-dark:disabled {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active,\n.show > .btn-dark.dropdown-toggle {\n color: #fff;\n background-color: #1d2124;\n border-color: #171a1d;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-outline-primary {\n color: #007bff;\n background-color: transparent;\n background-image: none;\n border-color: #007bff;\n}\n\n.btn-outline-primary:hover {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:focus, .btn-outline-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-primary.disabled, .btn-outline-primary:disabled {\n color: #007bff;\n background-color: transparent;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-primary.dropdown-toggle {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-secondary {\n color: #6c757d;\n background-color: transparent;\n background-image: none;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:hover {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:focus, .btn-outline-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-secondary.disabled, .btn-outline-secondary:disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-secondary.dropdown-toggle {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-success {\n color: #28a745;\n background-color: transparent;\n background-image: none;\n border-color: #28a745;\n}\n\n.btn-outline-success:hover {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:focus, .btn-outline-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-success.disabled, .btn-outline-success:disabled {\n color: #28a745;\n background-color: transparent;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active,\n.show > .btn-outline-success.dropdown-toggle {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-info {\n color: #17a2b8;\n background-color: transparent;\n background-image: none;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:hover {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:focus, .btn-outline-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-info.disabled, .btn-outline-info:disabled {\n color: #17a2b8;\n background-color: transparent;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,\n.show > .btn-outline-info.dropdown-toggle {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-warning {\n color: #ffc107;\n background-color: transparent;\n background-image: none;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:hover {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:focus, .btn-outline-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-warning.disabled, .btn-outline-warning:disabled {\n color: #ffc107;\n background-color: transparent;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active,\n.show > .btn-outline-warning.dropdown-toggle {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-danger {\n color: #dc3545;\n background-color: transparent;\n background-image: none;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:hover {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:focus, .btn-outline-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-danger.disabled, .btn-outline-danger:disabled {\n color: #dc3545;\n background-color: transparent;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active,\n.show > .btn-outline-danger.dropdown-toggle {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-light {\n color: #f8f9fa;\n background-color: transparent;\n background-image: none;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:hover {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:focus, .btn-outline-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-light.disabled, .btn-outline-light:disabled {\n color: #f8f9fa;\n background-color: transparent;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active,\n.show > .btn-outline-light.dropdown-toggle {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-dark {\n color: #343a40;\n background-color: transparent;\n background-image: none;\n border-color: #343a40;\n}\n\n.btn-outline-dark:hover {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:focus, .btn-outline-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-outline-dark.disabled, .btn-outline-dark:disabled {\n color: #343a40;\n background-color: transparent;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active,\n.show > .btn-outline-dark.dropdown-toggle {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-link {\n font-weight: 400;\n color: #007bff;\n background-color: transparent;\n}\n\n.btn-link:hover {\n color: #0056b3;\n text-decoration: underline;\n background-color: transparent;\n border-color: transparent;\n}\n\n.btn-link:focus, .btn-link.focus {\n text-decoration: underline;\n border-color: transparent;\n box-shadow: none;\n}\n\n.btn-link:disabled, .btn-link.disabled {\n color: #6c757d;\n pointer-events: none;\n}\n\n.btn-lg, .btn-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.btn-block {\n display: block;\n width: 100%;\n}\n\n.btn-block + .btn-block {\n margin-top: 0.5rem;\n}\n\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n\n.dropup,\n.dropright,\n.dropdown,\n.dropleft {\n position: relative;\n}\n\n.dropdown-toggle::after {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 10rem;\n padding: 0.5rem 0;\n margin: 0.125rem 0 0;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n\n.dropup .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: 0.125rem;\n}\n\n.dropup .dropdown-toggle::after {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-menu {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: 0.125rem;\n}\n\n.dropright .dropdown-toggle::after {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n\n.dropright .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropleft .dropdown-menu {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: 0.125rem;\n}\n\n.dropleft .dropdown-toggle::after {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n\n.dropleft .dropdown-toggle::after {\n display: none;\n}\n\n.dropleft .dropdown-toggle::before {\n display: inline-block;\n width: 0;\n height: 0;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n\n.dropleft .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-menu[x-placement^=\"top\"], .dropdown-menu[x-placement^=\"right\"], .dropdown-menu[x-placement^=\"bottom\"], .dropdown-menu[x-placement^=\"left\"] {\n right: auto;\n bottom: auto;\n}\n\n.dropdown-divider {\n height: 0;\n margin: 0.5rem 0;\n overflow: hidden;\n border-top: 1px solid #e9ecef;\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: 0.25rem 1.5rem;\n clear: both;\n font-weight: 400;\n color: #212529;\n text-align: inherit;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n}\n\n.dropdown-item:hover, .dropdown-item:focus {\n color: #16181b;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.dropdown-item.active, .dropdown-item:active {\n color: #fff;\n text-decoration: none;\n background-color: #007bff;\n}\n\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: 0.5rem 1.5rem;\n margin-bottom: 0;\n font-size: 0.875rem;\n color: #6c757d;\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: 0.25rem 1.5rem;\n color: #212529;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: -ms-inline-flexbox;\n display: inline-flex;\n vertical-align: middle;\n}\n\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n -ms-flex: 0 1 auto;\n flex: 0 1 auto;\n}\n\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover {\n z-index: 1;\n}\n\n.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group,\n.btn-group-vertical .btn + .btn,\n.btn-group-vertical .btn + .btn-group,\n.btn-group-vertical .btn-group + .btn,\n.btn-group-vertical .btn-group + .btn-group {\n margin-left: -1px;\n}\n\n.btn-toolbar {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n}\n\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n\n.dropdown-toggle-split::after,\n.dropup .dropdown-toggle-split::after,\n.dropright .dropdown-toggle-split::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n -ms-flex-direction: column;\n flex-direction: column;\n -ms-flex-align: start;\n align-items: flex-start;\n -ms-flex-pack: center;\n justify-content: center;\n}\n\n.btn-group-vertical .btn,\n.btn-group-vertical .btn-group {\n width: 100%;\n}\n\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.btn-group-toggle > .btn,\n.btn-group-toggle > .btn-group > .btn {\n margin-bottom: 0;\n}\n\n.btn-group-toggle > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn input[type=\"checkbox\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n\n.input-group {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-align: stretch;\n align-items: stretch;\n width: 100%;\n}\n\n.input-group > .form-control,\n.input-group > .custom-select,\n.input-group > .custom-file {\n position: relative;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n width: 1%;\n margin-bottom: 0;\n}\n\n.input-group > .form-control:focus,\n.input-group > .custom-select:focus,\n.input-group > .custom-file:focus {\n z-index: 3;\n}\n\n.input-group > .form-control + .form-control,\n.input-group > .form-control + .custom-select,\n.input-group > .form-control + .custom-file,\n.input-group > .custom-select + .form-control,\n.input-group > .custom-select + .custom-select,\n.input-group > .custom-select + .custom-file,\n.input-group > .custom-file + .form-control,\n.input-group > .custom-file + .custom-select,\n.input-group > .custom-file + .custom-file {\n margin-left: -1px;\n}\n\n.input-group > .form-control:not(:last-child),\n.input-group > .custom-select:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .form-control:not(:first-child),\n.input-group > .custom-select:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group > .custom-file {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n}\n\n.input-group > .custom-file:not(:last-child) .custom-file-label,\n.input-group > .custom-file:not(:last-child) .custom-file-label::after {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .custom-file:not(:first-child) .custom-file-label {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group-prepend,\n.input-group-append {\n display: -ms-flexbox;\n display: flex;\n}\n\n.input-group-prepend .btn,\n.input-group-append .btn {\n position: relative;\n z-index: 2;\n}\n\n.input-group-prepend .btn + .btn,\n.input-group-prepend .btn + .input-group-text,\n.input-group-prepend .input-group-text + .input-group-text,\n.input-group-prepend .input-group-text + .btn,\n.input-group-append .btn + .btn,\n.input-group-append .btn + .input-group-text,\n.input-group-append .input-group-text + .input-group-text,\n.input-group-append .input-group-text + .btn {\n margin-left: -1px;\n}\n\n.input-group-prepend {\n margin-right: -1px;\n}\n\n.input-group-append {\n margin-left: -1px;\n}\n\n.input-group-text {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n padding: 0.375rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n text-align: center;\n white-space: nowrap;\n background-color: #e9ecef;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.input-group-text input[type=\"radio\"],\n.input-group-text input[type=\"checkbox\"] {\n margin-top: 0;\n}\n\n.input-group > .input-group-prepend > .btn,\n.input-group > .input-group-prepend > .input-group-text,\n.input-group > .input-group-append:not(:last-child) > .btn,\n.input-group > .input-group-append:not(:last-child) > .input-group-text,\n.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .input-group-append > .btn,\n.input-group > .input-group-append > .input-group-text,\n.input-group > .input-group-prepend:not(:first-child) > .btn,\n.input-group > .input-group-prepend:not(:first-child) > .input-group-text,\n.input-group > .input-group-prepend:first-child > .btn:not(:first-child),\n.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.custom-control {\n position: relative;\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5rem;\n}\n\n.custom-control-inline {\n display: -ms-inline-flexbox;\n display: inline-flex;\n margin-right: 1rem;\n}\n\n.custom-control-input {\n position: absolute;\n z-index: -1;\n opacity: 0;\n}\n\n.custom-control-input:checked ~ .custom-control-label::before {\n color: #fff;\n background-color: #007bff;\n}\n\n.custom-control-input:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-control-input:active ~ .custom-control-label::before {\n color: #fff;\n background-color: #b3d7ff;\n}\n\n.custom-control-input:disabled ~ .custom-control-label {\n color: #6c757d;\n}\n\n.custom-control-input:disabled ~ .custom-control-label::before {\n background-color: #e9ecef;\n}\n\n.custom-control-label {\n position: relative;\n margin-bottom: 0;\n}\n\n.custom-control-label::before {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n pointer-events: none;\n content: \"\";\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n background-color: #dee2e6;\n}\n\n.custom-control-label::after {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n content: \"\";\n background-repeat: no-repeat;\n background-position: center center;\n background-size: 50% 50%;\n}\n\n.custom-checkbox .custom-control-label::before {\n border-radius: 0.25rem;\n}\n\n.custom-checkbox .custom-control-input:checked ~ .custom-control-label::before {\n background-color: #007bff;\n}\n\n.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E\");\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {\n background-color: #007bff;\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E\");\n}\n\n.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-radio .custom-control-label::before {\n border-radius: 50%;\n}\n\n.custom-radio .custom-control-input:checked ~ .custom-control-label::before {\n background-color: #007bff;\n}\n\n.custom-radio .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E\");\n}\n\n.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-select {\n display: inline-block;\n width: 100%;\n height: calc(2.25rem + 2px);\n padding: 0.375rem 1.75rem 0.375rem 0.75rem;\n line-height: 1.5;\n color: #495057;\n vertical-align: middle;\n background: #fff url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E\") no-repeat right 0.75rem center;\n background-size: 8px 10px;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n\n.custom-select:focus {\n border-color: #80bdff;\n outline: 0;\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.075), 0 0 5px rgba(128, 189, 255, 0.5);\n}\n\n.custom-select:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.custom-select[multiple], .custom-select[size]:not([size=\"1\"]) {\n height: auto;\n padding-right: 0.75rem;\n background-image: none;\n}\n\n.custom-select:disabled {\n color: #6c757d;\n background-color: #e9ecef;\n}\n\n.custom-select::-ms-expand {\n opacity: 0;\n}\n\n.custom-select-sm {\n height: calc(1.8125rem + 2px);\n padding-top: 0.375rem;\n padding-bottom: 0.375rem;\n font-size: 75%;\n}\n\n.custom-select-lg {\n height: calc(2.875rem + 2px);\n padding-top: 0.375rem;\n padding-bottom: 0.375rem;\n font-size: 125%;\n}\n\n.custom-file {\n position: relative;\n display: inline-block;\n width: 100%;\n height: calc(2.25rem + 2px);\n margin-bottom: 0;\n}\n\n.custom-file-input {\n position: relative;\n z-index: 2;\n width: 100%;\n height: calc(2.25rem + 2px);\n margin: 0;\n opacity: 0;\n}\n\n.custom-file-input:focus ~ .custom-file-label {\n border-color: #80bdff;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-file-input:focus ~ .custom-file-label::after {\n border-color: #80bdff;\n}\n\n.custom-file-input:lang(en) ~ .custom-file-label::after {\n content: \"Browse\";\n}\n\n.custom-file-label {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1;\n height: calc(2.25rem + 2px);\n padding: 0.375rem 0.75rem;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.custom-file-label::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 3;\n display: block;\n height: 2.25rem;\n padding: 0.375rem 0.75rem;\n line-height: 1.5;\n color: #495057;\n content: \"Browse\";\n background-color: #e9ecef;\n border-left: 1px solid #ced4da;\n border-radius: 0 0.25rem 0.25rem 0;\n}\n\n.custom-range {\n width: 100%;\n padding-left: 0;\n background-color: transparent;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n\n.custom-range:focus {\n outline: none;\n}\n\n.custom-range::-moz-focus-outer {\n border: 0;\n}\n\n.custom-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n -webkit-appearance: none;\n appearance: none;\n}\n\n.custom-range::-webkit-slider-thumb:focus {\n outline: none;\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range::-webkit-slider-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n -moz-appearance: none;\n appearance: none;\n}\n\n.custom-range::-moz-range-thumb:focus {\n outline: none;\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range::-moz-range-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n appearance: none;\n}\n\n.custom-range::-ms-thumb:focus {\n outline: none;\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range::-ms-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-ms-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: transparent;\n border-color: transparent;\n border-width: 0.5rem;\n}\n\n.custom-range::-ms-fill-lower {\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-fill-upper {\n margin-right: 15px;\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.nav {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: 0.5rem 1rem;\n}\n\n.nav-link:hover, .nav-link:focus {\n text-decoration: none;\n}\n\n.nav-link.disabled {\n color: #6c757d;\n}\n\n.nav-tabs {\n border-bottom: 1px solid #dee2e6;\n}\n\n.nav-tabs .nav-item {\n margin-bottom: -1px;\n}\n\n.nav-tabs .nav-link {\n border: 1px solid transparent;\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n border-color: #e9ecef #e9ecef #dee2e6;\n}\n\n.nav-tabs .nav-link.disabled {\n color: #6c757d;\n background-color: transparent;\n border-color: transparent;\n}\n\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: #495057;\n background-color: #fff;\n border-color: #dee2e6 #dee2e6 #fff;\n}\n\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills .nav-link {\n border-radius: 0.25rem;\n}\n\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: #fff;\n background-color: #007bff;\n}\n\n.nav-fill .nav-item {\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified .nav-item {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n text-align: center;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: justify;\n justify-content: space-between;\n padding: 0.5rem 1rem;\n}\n\n.navbar > .container,\n.navbar > .container-fluid {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: justify;\n justify-content: space-between;\n}\n\n.navbar-brand {\n display: inline-block;\n padding-top: 0.3125rem;\n padding-bottom: 0.3125rem;\n margin-right: 1rem;\n font-size: 1.25rem;\n line-height: inherit;\n white-space: nowrap;\n}\n\n.navbar-brand:hover, .navbar-brand:focus {\n text-decoration: none;\n}\n\n.navbar-nav {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.navbar-nav .nav-link {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-nav .dropdown-menu {\n position: static;\n float: none;\n}\n\n.navbar-text {\n display: inline-block;\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.navbar-collapse {\n -ms-flex-preferred-size: 100%;\n flex-basis: 100%;\n -ms-flex-positive: 1;\n flex-grow: 1;\n -ms-flex-align: center;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: 0.25rem 0.75rem;\n font-size: 1.25rem;\n line-height: 1;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.navbar-toggler:hover, .navbar-toggler:focus {\n text-decoration: none;\n}\n\n.navbar-toggler:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n content: \"\";\n background: no-repeat center center;\n background-size: 100% 100%;\n}\n\n@media (max-width: 575.98px) {\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-sm .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 767.98px) {\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 768px) {\n .navbar-expand-md {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-md .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 991.98px) {\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 992px) {\n .navbar-expand-lg {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-lg .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 1199.98px) {\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-xl .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n}\n\n.navbar-expand {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-expand .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n}\n\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n\n.navbar-expand .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n}\n\n.navbar-expand .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n}\n\n.navbar-expand .navbar-toggler {\n display: none;\n}\n\n.navbar-light .navbar-brand {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-nav .nav-link {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {\n color: rgba(0, 0, 0, 0.7);\n}\n\n.navbar-light .navbar-nav .nav-link.disabled {\n color: rgba(0, 0, 0, 0.3);\n}\n\n.navbar-light .navbar-nav .show > .nav-link,\n.navbar-light .navbar-nav .active > .nav-link,\n.navbar-light .navbar-nav .nav-link.show,\n.navbar-light .navbar-nav .nav-link.active {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-toggler {\n color: rgba(0, 0, 0, 0.5);\n border-color: rgba(0, 0, 0, 0.1);\n}\n\n.navbar-light .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E\");\n}\n\n.navbar-light .navbar-text {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-text a {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-dark .navbar-brand {\n color: #fff;\n}\n\n.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {\n color: #fff;\n}\n\n.navbar-dark .navbar-nav .nav-link {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {\n color: rgba(255, 255, 255, 0.75);\n}\n\n.navbar-dark .navbar-nav .nav-link.disabled {\n color: rgba(255, 255, 255, 0.25);\n}\n\n.navbar-dark .navbar-nav .show > .nav-link,\n.navbar-dark .navbar-nav .active > .nav-link,\n.navbar-dark .navbar-nav .nav-link.show,\n.navbar-dark .navbar-nav .nav-link.active {\n color: #fff;\n}\n\n.navbar-dark .navbar-toggler {\n color: rgba(255, 255, 255, 0.5);\n border-color: rgba(255, 255, 255, 0.1);\n}\n\n.navbar-dark .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E\");\n}\n\n.navbar-dark .navbar-text {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-text a {\n color: #fff;\n}\n\n.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus {\n color: #fff;\n}\n\n.card {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n min-width: 0;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: border-box;\n border: 1px solid rgba(0, 0, 0, 0.125);\n border-radius: 0.25rem;\n}\n\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n\n.card > .list-group:first-child .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.card > .list-group:last-child .list-group-item:last-child {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.card-body {\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n padding: 1.25rem;\n}\n\n.card-title {\n margin-bottom: 0.75rem;\n}\n\n.card-subtitle {\n margin-top: -0.375rem;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link:hover {\n text-decoration: none;\n}\n\n.card-link + .card-link {\n margin-left: 1.25rem;\n}\n\n.card-header {\n padding: 0.75rem 1.25rem;\n margin-bottom: 0;\n background-color: rgba(0, 0, 0, 0.03);\n border-bottom: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-header:first-child {\n border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;\n}\n\n.card-header + .list-group .list-group-item:first-child {\n border-top: 0;\n}\n\n.card-footer {\n padding: 0.75rem 1.25rem;\n background-color: rgba(0, 0, 0, 0.03);\n border-top: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-footer:last-child {\n border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);\n}\n\n.card-header-tabs {\n margin-right: -0.625rem;\n margin-bottom: -0.75rem;\n margin-left: -0.625rem;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -0.625rem;\n margin-left: -0.625rem;\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: 1.25rem;\n}\n\n.card-img {\n width: 100%;\n border-radius: calc(0.25rem - 1px);\n}\n\n.card-img-top {\n width: 100%;\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card-img-bottom {\n width: 100%;\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-deck {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n\n.card-deck .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-deck {\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n margin-right: -15px;\n margin-left: -15px;\n }\n .card-deck .card {\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 1 0 0%;\n flex: 1 0 0%;\n -ms-flex-direction: column;\n flex-direction: column;\n margin-right: 15px;\n margin-bottom: 0;\n margin-left: 15px;\n }\n}\n\n.card-group {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n\n.card-group > .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-group {\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n }\n .card-group > .card {\n -ms-flex: 1 0 0%;\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:first-child {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:first-child .card-img-top,\n .card-group > .card:first-child .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:first-child .card-img-bottom,\n .card-group > .card:first-child .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:last-child {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:last-child .card-img-top,\n .card-group > .card:last-child .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:last-child .card-img-bottom,\n .card-group > .card:last-child .card-footer {\n border-bottom-left-radius: 0;\n }\n .card-group > .card:only-child {\n border-radius: 0.25rem;\n }\n .card-group > .card:only-child .card-img-top,\n .card-group > .card:only-child .card-header {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n }\n .card-group > .card:only-child .card-img-bottom,\n .card-group > .card:only-child .card-footer {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n }\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) {\n border-radius: 0;\n }\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-top,\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-header,\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-footer {\n border-radius: 0;\n }\n}\n\n.card-columns .card {\n margin-bottom: 0.75rem;\n}\n\n@media (min-width: 576px) {\n .card-columns {\n -webkit-column-count: 3;\n -moz-column-count: 3;\n column-count: 3;\n -webkit-column-gap: 1.25rem;\n -moz-column-gap: 1.25rem;\n column-gap: 1.25rem;\n orphans: 1;\n widows: 1;\n }\n .card-columns .card {\n display: inline-block;\n width: 100%;\n }\n}\n\n.accordion .card:not(:first-of-type):not(:last-of-type) {\n border-bottom: 0;\n border-radius: 0;\n}\n\n.accordion .card:not(:first-of-type) .card-header:first-child {\n border-radius: 0;\n}\n\n.accordion .card:first-of-type {\n border-bottom: 0;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.accordion .card:last-of-type {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.breadcrumb {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n padding: 0.75rem 1rem;\n margin-bottom: 1rem;\n list-style: none;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: 0.5rem;\n}\n\n.breadcrumb-item + .breadcrumb-item::before {\n display: inline-block;\n padding-right: 0.5rem;\n color: #6c757d;\n content: \"/\";\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: underline;\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: none;\n}\n\n.breadcrumb-item.active {\n color: #6c757d;\n}\n\n.pagination {\n display: -ms-flexbox;\n display: flex;\n padding-left: 0;\n list-style: none;\n border-radius: 0.25rem;\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: 0.5rem 0.75rem;\n margin-left: -1px;\n line-height: 1.25;\n color: #007bff;\n background-color: #fff;\n border: 1px solid #dee2e6;\n}\n\n.page-link:hover {\n z-index: 2;\n color: #0056b3;\n text-decoration: none;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.page-link:focus {\n z-index: 2;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.page-link:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\n.page-item:first-child .page-link {\n margin-left: 0;\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.page-item:last-child .page-link {\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n}\n\n.page-item.active .page-link {\n z-index: 1;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.page-item.disabled .page-link {\n color: #6c757d;\n pointer-events: none;\n cursor: auto;\n background-color: #fff;\n border-color: #dee2e6;\n}\n\n.pagination-lg .page-link {\n padding: 0.75rem 1.5rem;\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.pagination-lg .page-item:first-child .page-link {\n border-top-left-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n\n.pagination-lg .page-item:last-child .page-link {\n border-top-right-radius: 0.3rem;\n border-bottom-right-radius: 0.3rem;\n}\n\n.pagination-sm .page-link {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.pagination-sm .page-item:first-child .page-link {\n border-top-left-radius: 0.2rem;\n border-bottom-left-radius: 0.2rem;\n}\n\n.pagination-sm .page-item:last-child .page-link {\n border-top-right-radius: 0.2rem;\n border-bottom-right-radius: 0.2rem;\n}\n\n.badge {\n display: inline-block;\n padding: 0.25em 0.4em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25rem;\n}\n\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.badge-pill {\n padding-right: 0.6em;\n padding-left: 0.6em;\n border-radius: 10rem;\n}\n\n.badge-primary {\n color: #fff;\n background-color: #007bff;\n}\n\n.badge-primary[href]:hover, .badge-primary[href]:focus {\n color: #fff;\n text-decoration: none;\n background-color: #0062cc;\n}\n\n.badge-secondary {\n color: #fff;\n background-color: #6c757d;\n}\n\n.badge-secondary[href]:hover, .badge-secondary[href]:focus {\n color: #fff;\n text-decoration: none;\n background-color: #545b62;\n}\n\n.badge-success {\n color: #fff;\n background-color: #28a745;\n}\n\n.badge-success[href]:hover, .badge-success[href]:focus {\n color: #fff;\n text-decoration: none;\n background-color: #1e7e34;\n}\n\n.badge-info {\n color: #fff;\n background-color: #17a2b8;\n}\n\n.badge-info[href]:hover, .badge-info[href]:focus {\n color: #fff;\n text-decoration: none;\n background-color: #117a8b;\n}\n\n.badge-warning {\n color: #212529;\n background-color: #ffc107;\n}\n\n.badge-warning[href]:hover, .badge-warning[href]:focus {\n color: #212529;\n text-decoration: none;\n background-color: #d39e00;\n}\n\n.badge-danger {\n color: #fff;\n background-color: #dc3545;\n}\n\n.badge-danger[href]:hover, .badge-danger[href]:focus {\n color: #fff;\n text-decoration: none;\n background-color: #bd2130;\n}\n\n.badge-light {\n color: #212529;\n background-color: #f8f9fa;\n}\n\n.badge-light[href]:hover, .badge-light[href]:focus {\n color: #212529;\n text-decoration: none;\n background-color: #dae0e5;\n}\n\n.badge-dark {\n color: #fff;\n background-color: #343a40;\n}\n\n.badge-dark[href]:hover, .badge-dark[href]:focus {\n color: #fff;\n text-decoration: none;\n background-color: #1d2124;\n}\n\n.jumbotron {\n padding: 2rem 1rem;\n margin-bottom: 2rem;\n background-color: #e9ecef;\n border-radius: 0.3rem;\n}\n\n@media (min-width: 576px) {\n .jumbotron {\n padding: 4rem 2rem;\n }\n}\n\n.jumbotron-fluid {\n padding-right: 0;\n padding-left: 0;\n border-radius: 0;\n}\n\n.alert {\n position: relative;\n padding: 0.75rem 1.25rem;\n margin-bottom: 1rem;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n}\n\n.alert-dismissible {\n padding-right: 4rem;\n}\n\n.alert-dismissible .close {\n position: absolute;\n top: 0;\n right: 0;\n padding: 0.75rem 1.25rem;\n color: inherit;\n}\n\n.alert-primary {\n color: #004085;\n background-color: #cce5ff;\n border-color: #b8daff;\n}\n\n.alert-primary hr {\n border-top-color: #9fcdff;\n}\n\n.alert-primary .alert-link {\n color: #002752;\n}\n\n.alert-secondary {\n color: #383d41;\n background-color: #e2e3e5;\n border-color: #d6d8db;\n}\n\n.alert-secondary hr {\n border-top-color: #c8cbcf;\n}\n\n.alert-secondary .alert-link {\n color: #202326;\n}\n\n.alert-success {\n color: #155724;\n background-color: #d4edda;\n border-color: #c3e6cb;\n}\n\n.alert-success hr {\n border-top-color: #b1dfbb;\n}\n\n.alert-success .alert-link {\n color: #0b2e13;\n}\n\n.alert-info {\n color: #0c5460;\n background-color: #d1ecf1;\n border-color: #bee5eb;\n}\n\n.alert-info hr {\n border-top-color: #abdde5;\n}\n\n.alert-info .alert-link {\n color: #062c33;\n}\n\n.alert-warning {\n color: #856404;\n background-color: #fff3cd;\n border-color: #ffeeba;\n}\n\n.alert-warning hr {\n border-top-color: #ffe8a1;\n}\n\n.alert-warning .alert-link {\n color: #533f03;\n}\n\n.alert-danger {\n color: #721c24;\n background-color: #f8d7da;\n border-color: #f5c6cb;\n}\n\n.alert-danger hr {\n border-top-color: #f1b0b7;\n}\n\n.alert-danger .alert-link {\n color: #491217;\n}\n\n.alert-light {\n color: #818182;\n background-color: #fefefe;\n border-color: #fdfdfe;\n}\n\n.alert-light hr {\n border-top-color: #ececf6;\n}\n\n.alert-light .alert-link {\n color: #686868;\n}\n\n.alert-dark {\n color: #1b1e21;\n background-color: #d6d8d9;\n border-color: #c6c8ca;\n}\n\n.alert-dark hr {\n border-top-color: #b9bbbe;\n}\n\n.alert-dark .alert-link {\n color: #040505;\n}\n\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 1rem 0;\n }\n to {\n background-position: 0 0;\n }\n}\n\n@keyframes progress-bar-stripes {\n from {\n background-position: 1rem 0;\n }\n to {\n background-position: 0 0;\n }\n}\n\n.progress {\n display: -ms-flexbox;\n display: flex;\n height: 1rem;\n overflow: hidden;\n font-size: 0.75rem;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.progress-bar {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n -ms-flex-pack: center;\n justify-content: center;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n background-color: #007bff;\n transition: width 0.6s ease;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 1rem 1rem;\n}\n\n.progress-bar-animated {\n -webkit-animation: progress-bar-stripes 1s linear infinite;\n animation: progress-bar-stripes 1s linear infinite;\n}\n\n.media {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: start;\n align-items: flex-start;\n}\n\n.media-body {\n -ms-flex: 1;\n flex: 1;\n}\n\n.list-group {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n}\n\n.list-group-item-action {\n width: 100%;\n color: #495057;\n text-align: inherit;\n}\n\n.list-group-item-action:hover, .list-group-item-action:focus {\n color: #495057;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.list-group-item-action:active {\n color: #212529;\n background-color: #e9ecef;\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 0.75rem 1.25rem;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.list-group-item:hover, .list-group-item:focus {\n z-index: 1;\n text-decoration: none;\n}\n\n.list-group-item.disabled, .list-group-item:disabled {\n color: #6c757d;\n background-color: #fff;\n}\n\n.list-group-item.active {\n z-index: 2;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.list-group-flush .list-group-item {\n border-right: 0;\n border-left: 0;\n border-radius: 0;\n}\n\n.list-group-flush:first-child .list-group-item:first-child {\n border-top: 0;\n}\n\n.list-group-flush:last-child .list-group-item:last-child {\n border-bottom: 0;\n}\n\n.list-group-item-primary {\n color: #004085;\n background-color: #b8daff;\n}\n\n.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {\n color: #004085;\n background-color: #9fcdff;\n}\n\n.list-group-item-primary.list-group-item-action.active {\n color: #fff;\n background-color: #004085;\n border-color: #004085;\n}\n\n.list-group-item-secondary {\n color: #383d41;\n background-color: #d6d8db;\n}\n\n.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {\n color: #383d41;\n background-color: #c8cbcf;\n}\n\n.list-group-item-secondary.list-group-item-action.active {\n color: #fff;\n background-color: #383d41;\n border-color: #383d41;\n}\n\n.list-group-item-success {\n color: #155724;\n background-color: #c3e6cb;\n}\n\n.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {\n color: #155724;\n background-color: #b1dfbb;\n}\n\n.list-group-item-success.list-group-item-action.active {\n color: #fff;\n background-color: #155724;\n border-color: #155724;\n}\n\n.list-group-item-info {\n color: #0c5460;\n background-color: #bee5eb;\n}\n\n.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {\n color: #0c5460;\n background-color: #abdde5;\n}\n\n.list-group-item-info.list-group-item-action.active {\n color: #fff;\n background-color: #0c5460;\n border-color: #0c5460;\n}\n\n.list-group-item-warning {\n color: #856404;\n background-color: #ffeeba;\n}\n\n.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {\n color: #856404;\n background-color: #ffe8a1;\n}\n\n.list-group-item-warning.list-group-item-action.active {\n color: #fff;\n background-color: #856404;\n border-color: #856404;\n}\n\n.list-group-item-danger {\n color: #721c24;\n background-color: #f5c6cb;\n}\n\n.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {\n color: #721c24;\n background-color: #f1b0b7;\n}\n\n.list-group-item-danger.list-group-item-action.active {\n color: #fff;\n background-color: #721c24;\n border-color: #721c24;\n}\n\n.list-group-item-light {\n color: #818182;\n background-color: #fdfdfe;\n}\n\n.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {\n color: #818182;\n background-color: #ececf6;\n}\n\n.list-group-item-light.list-group-item-action.active {\n color: #fff;\n background-color: #818182;\n border-color: #818182;\n}\n\n.list-group-item-dark {\n color: #1b1e21;\n background-color: #c6c8ca;\n}\n\n.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {\n color: #1b1e21;\n background-color: #b9bbbe;\n}\n\n.list-group-item-dark.list-group-item-action.active {\n color: #fff;\n background-color: #1b1e21;\n border-color: #1b1e21;\n}\n\n.close {\n float: right;\n font-size: 1.5rem;\n font-weight: 700;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n opacity: .5;\n}\n\n.close:hover, .close:focus {\n color: #000;\n text-decoration: none;\n opacity: .75;\n}\n\n.close:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\nbutton.close {\n padding: 0;\n background-color: transparent;\n border: 0;\n -webkit-appearance: none;\n}\n\n.modal-open {\n overflow: hidden;\n}\n\n.modal {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1050;\n display: none;\n overflow: hidden;\n outline: 0;\n}\n\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 0.5rem;\n pointer-events: none;\n}\n\n.modal.fade .modal-dialog {\n transition: -webkit-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out;\n -webkit-transform: translate(0, -25%);\n transform: translate(0, -25%);\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n\n.modal.show .modal-dialog {\n -webkit-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n\n.modal-dialog-centered {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n min-height: calc(100% - (0.5rem * 2));\n}\n\n.modal-content {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n width: 100%;\n pointer-events: auto;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n outline: 0;\n}\n\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n background-color: #000;\n}\n\n.modal-backdrop.fade {\n opacity: 0;\n}\n\n.modal-backdrop.show {\n opacity: 0.5;\n}\n\n.modal-header {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: start;\n align-items: flex-start;\n -ms-flex-pack: justify;\n justify-content: space-between;\n padding: 1rem;\n border-bottom: 1px solid #e9ecef;\n border-top-left-radius: 0.3rem;\n border-top-right-radius: 0.3rem;\n}\n\n.modal-header .close {\n padding: 1rem;\n margin: -1rem -1rem -1rem auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.modal-body {\n position: relative;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n padding: 1rem;\n}\n\n.modal-footer {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: end;\n justify-content: flex-end;\n padding: 1rem;\n border-top: 1px solid #e9ecef;\n}\n\n.modal-footer > :not(:first-child) {\n margin-left: .25rem;\n}\n\n.modal-footer > :not(:last-child) {\n margin-right: .25rem;\n}\n\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n\n@media (min-width: 576px) {\n .modal-dialog {\n max-width: 500px;\n margin: 1.75rem auto;\n }\n .modal-dialog-centered {\n min-height: calc(100% - (1.75rem * 2));\n }\n .modal-sm {\n max-width: 300px;\n }\n}\n\n@media (min-width: 992px) {\n .modal-lg {\n max-width: 800px;\n }\n}\n\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n opacity: 0;\n}\n\n.tooltip.show {\n opacity: 0.9;\n}\n\n.tooltip .arrow {\n position: absolute;\n display: block;\n width: 0.8rem;\n height: 0.4rem;\n}\n\n.tooltip .arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top, .bs-tooltip-auto[x-placement^=\"top\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^=\"top\"] .arrow {\n bottom: 0;\n}\n\n.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^=\"top\"] .arrow::before {\n top: 0;\n border-width: 0.4rem 0.4rem 0;\n border-top-color: #000;\n}\n\n.bs-tooltip-right, .bs-tooltip-auto[x-placement^=\"right\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^=\"right\"] .arrow {\n left: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^=\"right\"] .arrow::before {\n right: 0;\n border-width: 0.4rem 0.4rem 0.4rem 0;\n border-right-color: #000;\n}\n\n.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^=\"bottom\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow {\n top: 0;\n}\n\n.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow::before {\n bottom: 0;\n border-width: 0 0.4rem 0.4rem;\n border-bottom-color: #000;\n}\n\n.bs-tooltip-left, .bs-tooltip-auto[x-placement^=\"left\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^=\"left\"] .arrow {\n right: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^=\"left\"] .arrow::before {\n left: 0;\n border-width: 0.4rem 0 0.4rem 0.4rem;\n border-left-color: #000;\n}\n\n.tooltip-inner {\n max-width: 200px;\n padding: 0.25rem 0.5rem;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 0.25rem;\n}\n\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: block;\n max-width: 276px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n}\n\n.popover .arrow {\n position: absolute;\n display: block;\n width: 1rem;\n height: 0.5rem;\n margin: 0 0.3rem;\n}\n\n.popover .arrow::before, .popover .arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-popover-top, .bs-popover-auto[x-placement^=\"top\"] {\n margin-bottom: 0.5rem;\n}\n\n.bs-popover-top .arrow, .bs-popover-auto[x-placement^=\"top\"] .arrow {\n bottom: calc((0.5rem + 1px) * -1);\n}\n\n.bs-popover-top .arrow::before, .bs-popover-auto[x-placement^=\"top\"] .arrow::before,\n.bs-popover-top .arrow::after,\n.bs-popover-auto[x-placement^=\"top\"] .arrow::after {\n border-width: 0.5rem 0.5rem 0;\n}\n\n.bs-popover-top .arrow::before, .bs-popover-auto[x-placement^=\"top\"] .arrow::before {\n bottom: 0;\n border-top-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-top .arrow::after,\n.bs-popover-auto[x-placement^=\"top\"] .arrow::after {\n bottom: 1px;\n border-top-color: #fff;\n}\n\n.bs-popover-right, .bs-popover-auto[x-placement^=\"right\"] {\n margin-left: 0.5rem;\n}\n\n.bs-popover-right .arrow, .bs-popover-auto[x-placement^=\"right\"] .arrow {\n left: calc((0.5rem + 1px) * -1);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-right .arrow::before, .bs-popover-auto[x-placement^=\"right\"] .arrow::before,\n.bs-popover-right .arrow::after,\n.bs-popover-auto[x-placement^=\"right\"] .arrow::after {\n border-width: 0.5rem 0.5rem 0.5rem 0;\n}\n\n.bs-popover-right .arrow::before, .bs-popover-auto[x-placement^=\"right\"] .arrow::before {\n left: 0;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-right .arrow::after,\n.bs-popover-auto[x-placement^=\"right\"] .arrow::after {\n left: 1px;\n border-right-color: #fff;\n}\n\n.bs-popover-bottom, .bs-popover-auto[x-placement^=\"bottom\"] {\n margin-top: 0.5rem;\n}\n\n.bs-popover-bottom .arrow, .bs-popover-auto[x-placement^=\"bottom\"] .arrow {\n top: calc((0.5rem + 1px) * -1);\n}\n\n.bs-popover-bottom .arrow::before, .bs-popover-auto[x-placement^=\"bottom\"] .arrow::before,\n.bs-popover-bottom .arrow::after,\n.bs-popover-auto[x-placement^=\"bottom\"] .arrow::after {\n border-width: 0 0.5rem 0.5rem 0.5rem;\n}\n\n.bs-popover-bottom .arrow::before, .bs-popover-auto[x-placement^=\"bottom\"] .arrow::before {\n top: 0;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-bottom .arrow::after,\n.bs-popover-auto[x-placement^=\"bottom\"] .arrow::after {\n top: 1px;\n border-bottom-color: #fff;\n}\n\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^=\"bottom\"] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: 1rem;\n margin-left: -0.5rem;\n content: \"\";\n border-bottom: 1px solid #f7f7f7;\n}\n\n.bs-popover-left, .bs-popover-auto[x-placement^=\"left\"] {\n margin-right: 0.5rem;\n}\n\n.bs-popover-left .arrow, .bs-popover-auto[x-placement^=\"left\"] .arrow {\n right: calc((0.5rem + 1px) * -1);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-left .arrow::before, .bs-popover-auto[x-placement^=\"left\"] .arrow::before,\n.bs-popover-left .arrow::after,\n.bs-popover-auto[x-placement^=\"left\"] .arrow::after {\n border-width: 0.5rem 0 0.5rem 0.5rem;\n}\n\n.bs-popover-left .arrow::before, .bs-popover-auto[x-placement^=\"left\"] .arrow::before {\n right: 0;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-left .arrow::after,\n.bs-popover-auto[x-placement^=\"left\"] .arrow::after {\n right: 1px;\n border-left-color: #fff;\n}\n\n.popover-header {\n padding: 0.5rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n color: inherit;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: 0.5rem 0.75rem;\n color: #212529;\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.carousel-item {\n position: relative;\n display: none;\n -ms-flex-align: center;\n align-items: center;\n width: 100%;\n transition: -webkit-transform 0.6s ease;\n transition: transform 0.6s ease;\n transition: transform 0.6s ease, -webkit-transform 0.6s ease;\n -webkit-backface-visibility: hidden;\n backface-visibility: hidden;\n -webkit-perspective: 1000px;\n perspective: 1000px;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next,\n.carousel-item-prev {\n position: absolute;\n top: 0;\n}\n\n.carousel-item-next.carousel-item-left,\n.carousel-item-prev.carousel-item-right {\n -webkit-transform: translateX(0);\n transform: translateX(0);\n}\n\n@supports ((-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d)) {\n .carousel-item-next.carousel-item-left,\n .carousel-item-prev.carousel-item-right {\n -webkit-transform: translate3d(0, 0, 0);\n transform: translate3d(0, 0, 0);\n }\n}\n\n.carousel-item-next,\n.active.carousel-item-right {\n -webkit-transform: translateX(100%);\n transform: translateX(100%);\n}\n\n@supports ((-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d)) {\n .carousel-item-next,\n .active.carousel-item-right {\n -webkit-transform: translate3d(100%, 0, 0);\n transform: translate3d(100%, 0, 0);\n }\n}\n\n.carousel-item-prev,\n.active.carousel-item-left {\n -webkit-transform: translateX(-100%);\n transform: translateX(-100%);\n}\n\n@supports ((-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d)) {\n .carousel-item-prev,\n .active.carousel-item-left {\n -webkit-transform: translate3d(-100%, 0, 0);\n transform: translate3d(-100%, 0, 0);\n }\n}\n\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-duration: .6s;\n transition-property: opacity;\n}\n\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-left,\n.carousel-fade .carousel-item-prev.carousel-item-right {\n opacity: 1;\n}\n\n.carousel-fade .active.carousel-item-left,\n.carousel-fade .active.carousel-item-right {\n opacity: 0;\n}\n\n.carousel-fade .carousel-item-next,\n.carousel-fade .carousel-item-prev,\n.carousel-fade .carousel-item.active,\n.carousel-fade .active.carousel-item-left,\n.carousel-fade .active.carousel-item-prev {\n -webkit-transform: translateX(0);\n transform: translateX(0);\n}\n\n@supports ((-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d)) {\n .carousel-fade .carousel-item-next,\n .carousel-fade .carousel-item-prev,\n .carousel-fade .carousel-item.active,\n .carousel-fade .active.carousel-item-left,\n .carousel-fade .active.carousel-item-prev {\n -webkit-transform: translate3d(0, 0, 0);\n transform: translate3d(0, 0, 0);\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n width: 15%;\n color: #fff;\n text-align: center;\n opacity: 0.5;\n}\n\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: .9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 20px;\n height: 20px;\n background: transparent no-repeat center center;\n background-size: 100% 100%;\n}\n\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E\");\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E\");\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 10px;\n left: 0;\n z-index: 15;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-pack: center;\n justify-content: center;\n padding-left: 0;\n margin-right: 15%;\n margin-left: 15%;\n list-style: none;\n}\n\n.carousel-indicators li {\n position: relative;\n -ms-flex: 0 1 auto;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: rgba(255, 255, 255, 0.5);\n}\n\n.carousel-indicators li::before {\n position: absolute;\n top: -10px;\n left: 0;\n display: inline-block;\n width: 100%;\n height: 10px;\n content: \"\";\n}\n\n.carousel-indicators li::after {\n position: absolute;\n bottom: -10px;\n left: 0;\n display: inline-block;\n width: 100%;\n height: 10px;\n content: \"\";\n}\n\n.carousel-indicators .active {\n background-color: #fff;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.bg-primary {\n background-color: #007bff !important;\n}\n\na.bg-primary:hover, a.bg-primary:focus,\nbutton.bg-primary:hover,\nbutton.bg-primary:focus {\n background-color: #0062cc !important;\n}\n\n.bg-secondary {\n background-color: #6c757d !important;\n}\n\na.bg-secondary:hover, a.bg-secondary:focus,\nbutton.bg-secondary:hover,\nbutton.bg-secondary:focus {\n background-color: #545b62 !important;\n}\n\n.bg-success {\n background-color: #28a745 !important;\n}\n\na.bg-success:hover, a.bg-success:focus,\nbutton.bg-success:hover,\nbutton.bg-success:focus {\n background-color: #1e7e34 !important;\n}\n\n.bg-info {\n background-color: #17a2b8 !important;\n}\n\na.bg-info:hover, a.bg-info:focus,\nbutton.bg-info:hover,\nbutton.bg-info:focus {\n background-color: #117a8b !important;\n}\n\n.bg-warning {\n background-color: #ffc107 !important;\n}\n\na.bg-warning:hover, a.bg-warning:focus,\nbutton.bg-warning:hover,\nbutton.bg-warning:focus {\n background-color: #d39e00 !important;\n}\n\n.bg-danger {\n background-color: #dc3545 !important;\n}\n\na.bg-danger:hover, a.bg-danger:focus,\nbutton.bg-danger:hover,\nbutton.bg-danger:focus {\n background-color: #bd2130 !important;\n}\n\n.bg-light {\n background-color: #f8f9fa !important;\n}\n\na.bg-light:hover, a.bg-light:focus,\nbutton.bg-light:hover,\nbutton.bg-light:focus {\n background-color: #dae0e5 !important;\n}\n\n.bg-dark {\n background-color: #343a40 !important;\n}\n\na.bg-dark:hover, a.bg-dark:focus,\nbutton.bg-dark:hover,\nbutton.bg-dark:focus {\n background-color: #1d2124 !important;\n}\n\n.bg-white {\n background-color: #fff !important;\n}\n\n.bg-transparent {\n background-color: transparent !important;\n}\n\n.border {\n border: 1px solid #dee2e6 !important;\n}\n\n.border-top {\n border-top: 1px solid #dee2e6 !important;\n}\n\n.border-right {\n border-right: 1px solid #dee2e6 !important;\n}\n\n.border-bottom {\n border-bottom: 1px solid #dee2e6 !important;\n}\n\n.border-left {\n border-left: 1px solid #dee2e6 !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-right-0 {\n border-right: 0 !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-left-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n border-color: #007bff !important;\n}\n\n.border-secondary {\n border-color: #6c757d !important;\n}\n\n.border-success {\n border-color: #28a745 !important;\n}\n\n.border-info {\n border-color: #17a2b8 !important;\n}\n\n.border-warning {\n border-color: #ffc107 !important;\n}\n\n.border-danger {\n border-color: #dc3545 !important;\n}\n\n.border-light {\n border-color: #f8f9fa !important;\n}\n\n.border-dark {\n border-color: #343a40 !important;\n}\n\n.border-white {\n border-color: #fff !important;\n}\n\n.rounded {\n border-radius: 0.25rem !important;\n}\n\n.rounded-top {\n border-top-left-radius: 0.25rem !important;\n border-top-right-radius: 0.25rem !important;\n}\n\n.rounded-right {\n border-top-right-radius: 0.25rem !important;\n border-bottom-right-radius: 0.25rem !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-left {\n border-top-left-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n}\n\n.d-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-md-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-print-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n.embed-responsive {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n}\n\n.embed-responsive::before {\n display: block;\n content: \"\";\n}\n\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n\n.embed-responsive-21by9::before {\n padding-top: 42.857143%;\n}\n\n.embed-responsive-16by9::before {\n padding-top: 56.25%;\n}\n\n.embed-responsive-4by3::before {\n padding-top: 75%;\n}\n\n.embed-responsive-1by1::before {\n padding-top: 100%;\n}\n\n.flex-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n}\n\n.flex-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n}\n\n.justify-content-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n}\n\n.align-items-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n}\n\n.align-items-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n}\n\n.align-items-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n}\n\n.align-items-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n}\n\n.align-content-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n}\n\n.align-content-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n}\n\n.align-content-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n}\n\n.align-content-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n}\n\n.align-content-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n}\n\n.align-self-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n}\n\n.align-self-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n}\n\n.align-self-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n}\n\n.align-self-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n}\n\n.align-self-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-sm-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-sm-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-sm-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-sm-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-sm-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-sm-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-sm-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-sm-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-md-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-md-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-md-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-md-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-md-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-md-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-md-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-md-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-md-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-md-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-md-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-md-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-md-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-md-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-md-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-md-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-lg-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-lg-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-lg-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-lg-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-lg-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-lg-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-lg-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-lg-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-xl-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-xl-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-xl-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-xl-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-xl-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-xl-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-xl-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-xl-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n.float-left {\n float: left !important;\n}\n\n.float-right {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-left {\n float: left !important;\n }\n .float-sm-right {\n float: right !important;\n }\n .float-sm-none {\n float: none !important;\n }\n}\n\n@media (min-width: 768px) {\n .float-md-left {\n float: left !important;\n }\n .float-md-right {\n float: right !important;\n }\n .float-md-none {\n float: none !important;\n }\n}\n\n@media (min-width: 992px) {\n .float-lg-left {\n float: left !important;\n }\n .float-lg-right {\n float: right !important;\n }\n .float-lg-none {\n float: none !important;\n }\n}\n\n@media (min-width: 1200px) {\n .float-xl-left {\n float: left !important;\n }\n .float-xl-right {\n float: right !important;\n }\n .float-xl-none {\n float: none !important;\n }\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: -webkit-sticky !important;\n position: sticky !important;\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n@supports ((position: -webkit-sticky) or (position: sticky)) {\n .sticky-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.sr-only-focusable:active, .sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n overflow: visible;\n clip: auto;\n white-space: normal;\n}\n\n.shadow-sm {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n}\n\n.shadow {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n.shadow-lg {\n box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n\n.text-monospace {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n.text-justify {\n text-align: justify !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.text-left {\n text-align: left !important;\n}\n\n.text-right {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n@media (min-width: 576px) {\n .text-sm-left {\n text-align: left !important;\n }\n .text-sm-right {\n text-align: right !important;\n }\n .text-sm-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 768px) {\n .text-md-left {\n text-align: left !important;\n }\n .text-md-right {\n text-align: right !important;\n }\n .text-md-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 992px) {\n .text-lg-left {\n text-align: left !important;\n }\n .text-lg-right {\n text-align: right !important;\n }\n .text-lg-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 1200px) {\n .text-xl-left {\n text-align: left !important;\n }\n .text-xl-right {\n text-align: right !important;\n }\n .text-xl-center {\n text-align: center !important;\n }\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.font-weight-light {\n font-weight: 300 !important;\n}\n\n.font-weight-normal {\n font-weight: 400 !important;\n}\n\n.font-weight-bold {\n font-weight: 700 !important;\n}\n\n.font-italic {\n font-style: italic !important;\n}\n\n.text-white {\n color: #fff !important;\n}\n\n.text-primary {\n color: #007bff !important;\n}\n\na.text-primary:hover, a.text-primary:focus {\n color: #0062cc !important;\n}\n\n.text-secondary {\n color: #6c757d !important;\n}\n\na.text-secondary:hover, a.text-secondary:focus {\n color: #545b62 !important;\n}\n\n.text-success {\n color: #28a745 !important;\n}\n\na.text-success:hover, a.text-success:focus {\n color: #1e7e34 !important;\n}\n\n.text-info {\n color: #17a2b8 !important;\n}\n\na.text-info:hover, a.text-info:focus {\n color: #117a8b !important;\n}\n\n.text-warning {\n color: #ffc107 !important;\n}\n\na.text-warning:hover, a.text-warning:focus {\n color: #d39e00 !important;\n}\n\n.text-danger {\n color: #dc3545 !important;\n}\n\na.text-danger:hover, a.text-danger:focus {\n color: #bd2130 !important;\n}\n\n.text-light {\n color: #f8f9fa !important;\n}\n\na.text-light:hover, a.text-light:focus {\n color: #dae0e5 !important;\n}\n\n.text-dark {\n color: #343a40 !important;\n}\n\na.text-dark:hover, a.text-dark:focus {\n color: #1d2124 !important;\n}\n\n.text-body {\n color: #212529 !important;\n}\n\n.text-muted {\n color: #6c757d !important;\n}\n\n.text-black-50 {\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n@media print {\n *,\n *::before,\n *::after {\n text-shadow: none !important;\n box-shadow: none !important;\n }\n a:not(.btn) {\n text-decoration: underline;\n }\n abbr[title]::after {\n content: \" (\" attr(title) \")\";\n }\n pre {\n white-space: pre-wrap !important;\n }\n pre,\n blockquote {\n border: 1px solid #adb5bd;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n @page {\n size: a3;\n }\n body {\n min-width: 992px !important;\n }\n .container {\n min-width: 992px !important;\n }\n .navbar {\n display: none;\n }\n .badge {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #dee2e6 !important;\n }\n .table-dark {\n color: inherit;\n }\n .table-dark th,\n .table-dark td,\n .table-dark thead th,\n .table-dark tbody + tbody {\n border-color: #dee2e6;\n }\n .table .thead-dark th {\n color: inherit;\n border-color: #dee2e6;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","/*!\n * Bootstrap v4.1.1 (https://getbootstrap.com/)\n * Copyright 2011-2018 The Bootstrap Authors\n * Copyright 2011-2018 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n:root {\n --blue: #007bff;\n --indigo: #6610f2;\n --purple: #6f42c1;\n --pink: #e83e8c;\n --red: #dc3545;\n --orange: #fd7e14;\n --yellow: #ffc107;\n --green: #28a745;\n --teal: #20c997;\n --cyan: #17a2b8;\n --white: #fff;\n --gray: #6c757d;\n --gray-dark: #343a40;\n --primary: #007bff;\n --secondary: #6c757d;\n --success: #28a745;\n --info: #17a2b8;\n --warning: #ffc107;\n --danger: #dc3545;\n --light: #f8f9fa;\n --dark: #343a40;\n --breakpoint-xs: 0;\n --breakpoint-sm: 576px;\n --breakpoint-md: 768px;\n --breakpoint-lg: 992px;\n --breakpoint-xl: 1200px;\n --font-family-sans-serif: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -ms-text-size-adjust: 100%;\n -ms-overflow-style: scrollbar;\n -webkit-tap-highlight-color: transparent;\n}\n\n@-ms-viewport {\n width: device-width;\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n -webkit-text-decoration-skip: objects;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):focus {\n outline: 0;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n -ms-overflow-style: scrollbar;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nbutton,\nhtml [type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-cancel-button,\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: 0.5rem;\n font-family: inherit;\n font-weight: 500;\n line-height: 1.2;\n color: inherit;\n}\n\nh1, .h1 {\n font-size: 2.5rem;\n}\n\nh2, .h2 {\n font-size: 2rem;\n}\n\nh3, .h3 {\n font-size: 1.75rem;\n}\n\nh4, .h4 {\n font-size: 1.5rem;\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: 6rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-2 {\n font-size: 5.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-3 {\n font-size: 4.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-4 {\n font-size: 3.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\nhr {\n margin-top: 1rem;\n margin-bottom: 1rem;\n border: 0;\n border-top: 1px solid rgba(0, 0, 0, 0.1);\n}\n\nsmall,\n.small {\n font-size: 80%;\n font-weight: 400;\n}\n\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n\n.blockquote-footer {\n display: block;\n font-size: 80%;\n color: #6c757d;\n}\n\n.blockquote-footer::before {\n content: \"\\2014 \\00A0\";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: #fff;\n border: 1px solid #dee2e6;\n border-radius: 0.25rem;\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 90%;\n color: #6c757d;\n}\n\ncode {\n font-size: 87.5%;\n color: #e83e8c;\n word-break: break-word;\n}\n\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.2rem 0.4rem;\n font-size: 87.5%;\n color: #fff;\n background-color: #212529;\n border-radius: 0.2rem;\n}\n\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n}\n\npre {\n display: block;\n font-size: 87.5%;\n color: #212529;\n}\n\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n\n.container {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container {\n max-width: 1140px;\n }\n}\n\n.container-fluid {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n.row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,\n.col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm,\n.col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md,\n.col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg,\n.col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl,\n.col-xl-auto {\n position: relative;\n width: 100%;\n min-height: 1px;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: none;\n}\n\n.col-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n order: -1;\n}\n\n.order-last {\n order: 13;\n}\n\n.order-0 {\n order: 0;\n}\n\n.order-1 {\n order: 1;\n}\n\n.order-2 {\n order: 2;\n}\n\n.order-3 {\n order: 3;\n}\n\n.order-4 {\n order: 4;\n}\n\n.order-5 {\n order: 5;\n}\n\n.order-6 {\n order: 6;\n}\n\n.order-7 {\n order: 7;\n}\n\n.order-8 {\n order: 8;\n}\n\n.order-9 {\n order: 9;\n}\n\n.order-10 {\n order: 10;\n}\n\n.order-11 {\n order: 11;\n}\n\n.order-12 {\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: none;\n }\n .col-sm-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n order: -1;\n }\n .order-sm-last {\n order: 13;\n }\n .order-sm-0 {\n order: 0;\n }\n .order-sm-1 {\n order: 1;\n }\n .order-sm-2 {\n order: 2;\n }\n .order-sm-3 {\n order: 3;\n }\n .order-sm-4 {\n order: 4;\n }\n .order-sm-5 {\n order: 5;\n }\n .order-sm-6 {\n order: 6;\n }\n .order-sm-7 {\n order: 7;\n }\n .order-sm-8 {\n order: 8;\n }\n .order-sm-9 {\n order: 9;\n }\n .order-sm-10 {\n order: 10;\n }\n .order-sm-11 {\n order: 11;\n }\n .order-sm-12 {\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: none;\n }\n .col-md-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n order: -1;\n }\n .order-md-last {\n order: 13;\n }\n .order-md-0 {\n order: 0;\n }\n .order-md-1 {\n order: 1;\n }\n .order-md-2 {\n order: 2;\n }\n .order-md-3 {\n order: 3;\n }\n .order-md-4 {\n order: 4;\n }\n .order-md-5 {\n order: 5;\n }\n .order-md-6 {\n order: 6;\n }\n .order-md-7 {\n order: 7;\n }\n .order-md-8 {\n order: 8;\n }\n .order-md-9 {\n order: 9;\n }\n .order-md-10 {\n order: 10;\n }\n .order-md-11 {\n order: 11;\n }\n .order-md-12 {\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: none;\n }\n .col-lg-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n order: -1;\n }\n .order-lg-last {\n order: 13;\n }\n .order-lg-0 {\n order: 0;\n }\n .order-lg-1 {\n order: 1;\n }\n .order-lg-2 {\n order: 2;\n }\n .order-lg-3 {\n order: 3;\n }\n .order-lg-4 {\n order: 4;\n }\n .order-lg-5 {\n order: 5;\n }\n .order-lg-6 {\n order: 6;\n }\n .order-lg-7 {\n order: 7;\n }\n .order-lg-8 {\n order: 8;\n }\n .order-lg-9 {\n order: 9;\n }\n .order-lg-10 {\n order: 10;\n }\n .order-lg-11 {\n order: 11;\n }\n .order-lg-12 {\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: none;\n }\n .col-xl-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n order: -1;\n }\n .order-xl-last {\n order: 13;\n }\n .order-xl-0 {\n order: 0;\n }\n .order-xl-1 {\n order: 1;\n }\n .order-xl-2 {\n order: 2;\n }\n .order-xl-3 {\n order: 3;\n }\n .order-xl-4 {\n order: 4;\n }\n .order-xl-5 {\n order: 5;\n }\n .order-xl-6 {\n order: 6;\n }\n .order-xl-7 {\n order: 7;\n }\n .order-xl-8 {\n order: 8;\n }\n .order-xl-9 {\n order: 9;\n }\n .order-xl-10 {\n order: 10;\n }\n .order-xl-11 {\n order: 11;\n }\n .order-xl-12 {\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 1rem;\n background-color: transparent;\n}\n\n.table th,\n.table td {\n padding: 0.75rem;\n vertical-align: top;\n border-top: 1px solid #dee2e6;\n}\n\n.table thead th {\n vertical-align: bottom;\n border-bottom: 2px solid #dee2e6;\n}\n\n.table tbody + tbody {\n border-top: 2px solid #dee2e6;\n}\n\n.table .table {\n background-color: #fff;\n}\n\n.table-sm th,\n.table-sm td {\n padding: 0.3rem;\n}\n\n.table-bordered {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered th,\n.table-bordered td {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered thead th,\n.table-bordered thead td {\n border-bottom-width: 2px;\n}\n\n.table-borderless th,\n.table-borderless td,\n.table-borderless thead th,\n.table-borderless tbody + tbody {\n border: 0;\n}\n\n.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n.table-hover tbody tr:hover {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-primary,\n.table-primary > th,\n.table-primary > td {\n background-color: #b8daff;\n}\n\n.table-hover .table-primary:hover {\n background-color: #9fcdff;\n}\n\n.table-hover .table-primary:hover > td,\n.table-hover .table-primary:hover > th {\n background-color: #9fcdff;\n}\n\n.table-secondary,\n.table-secondary > th,\n.table-secondary > td {\n background-color: #d6d8db;\n}\n\n.table-hover .table-secondary:hover {\n background-color: #c8cbcf;\n}\n\n.table-hover .table-secondary:hover > td,\n.table-hover .table-secondary:hover > th {\n background-color: #c8cbcf;\n}\n\n.table-success,\n.table-success > th,\n.table-success > td {\n background-color: #c3e6cb;\n}\n\n.table-hover .table-success:hover {\n background-color: #b1dfbb;\n}\n\n.table-hover .table-success:hover > td,\n.table-hover .table-success:hover > th {\n background-color: #b1dfbb;\n}\n\n.table-info,\n.table-info > th,\n.table-info > td {\n background-color: #bee5eb;\n}\n\n.table-hover .table-info:hover {\n background-color: #abdde5;\n}\n\n.table-hover .table-info:hover > td,\n.table-hover .table-info:hover > th {\n background-color: #abdde5;\n}\n\n.table-warning,\n.table-warning > th,\n.table-warning > td {\n background-color: #ffeeba;\n}\n\n.table-hover .table-warning:hover {\n background-color: #ffe8a1;\n}\n\n.table-hover .table-warning:hover > td,\n.table-hover .table-warning:hover > th {\n background-color: #ffe8a1;\n}\n\n.table-danger,\n.table-danger > th,\n.table-danger > td {\n background-color: #f5c6cb;\n}\n\n.table-hover .table-danger:hover {\n background-color: #f1b0b7;\n}\n\n.table-hover .table-danger:hover > td,\n.table-hover .table-danger:hover > th {\n background-color: #f1b0b7;\n}\n\n.table-light,\n.table-light > th,\n.table-light > td {\n background-color: #fdfdfe;\n}\n\n.table-hover .table-light:hover {\n background-color: #ececf6;\n}\n\n.table-hover .table-light:hover > td,\n.table-hover .table-light:hover > th {\n background-color: #ececf6;\n}\n\n.table-dark,\n.table-dark > th,\n.table-dark > td {\n background-color: #c6c8ca;\n}\n\n.table-hover .table-dark:hover {\n background-color: #b9bbbe;\n}\n\n.table-hover .table-dark:hover > td,\n.table-hover .table-dark:hover > th {\n background-color: #b9bbbe;\n}\n\n.table-active,\n.table-active > th,\n.table-active > td {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover > td,\n.table-hover .table-active:hover > th {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table .thead-dark th {\n color: #fff;\n background-color: #212529;\n border-color: #32383e;\n}\n\n.table .thead-light th {\n color: #495057;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.table-dark {\n color: #fff;\n background-color: #212529;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th {\n border-color: #32383e;\n}\n\n.table-dark.table-bordered {\n border: 0;\n}\n\n.table-dark.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(255, 255, 255, 0.05);\n}\n\n.table-dark.table-hover tbody tr:hover {\n background-color: rgba(255, 255, 255, 0.075);\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-sm > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 767.98px) {\n .table-responsive-md {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-md > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-lg > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n }\n .table-responsive-xl > .table-bordered {\n border: 0;\n }\n}\n\n.table-responsive {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n}\n\n.table-responsive > .table-bordered {\n border: 0;\n}\n\n.form-control {\n display: block;\n width: 100%;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n\n.form-control:focus {\n color: #495057;\n background-color: #fff;\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.form-control::placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control:disabled, .form-control[readonly] {\n background-color: #e9ecef;\n opacity: 1;\n}\n\nselect.form-control:not([size]):not([multiple]) {\n height: calc(2.25rem + 2px);\n}\n\nselect.form-control:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + 1px);\n padding-bottom: calc(0.375rem + 1px);\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + 1px);\n padding-bottom: calc(0.5rem + 1px);\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + 1px);\n padding-bottom: calc(0.25rem + 1px);\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding-top: 0.375rem;\n padding-bottom: 0.375rem;\n margin-bottom: 0;\n line-height: 1.5;\n color: #212529;\n background-color: transparent;\n border: solid transparent;\n border-width: 1px 0;\n}\n\n.form-control-plaintext.form-control-sm, .input-group-sm > .form-control-plaintext.form-control,\n.input-group-sm > .input-group-prepend > .form-control-plaintext.input-group-text,\n.input-group-sm > .input-group-append > .form-control-plaintext.input-group-text,\n.input-group-sm > .input-group-prepend > .form-control-plaintext.btn,\n.input-group-sm > .input-group-append > .form-control-plaintext.btn, .form-control-plaintext.form-control-lg, .input-group-lg > .form-control-plaintext.form-control,\n.input-group-lg > .input-group-prepend > .form-control-plaintext.input-group-text,\n.input-group-lg > .input-group-append > .form-control-plaintext.input-group-text,\n.input-group-lg > .input-group-prepend > .form-control-plaintext.btn,\n.input-group-lg > .input-group-append > .form-control-plaintext.btn {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm, .input-group-sm > .form-control,\n.input-group-sm > .input-group-prepend > .input-group-text,\n.input-group-sm > .input-group-append > .input-group-text,\n.input-group-sm > .input-group-prepend > .btn,\n.input-group-sm > .input-group-append > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\nselect.form-control-sm:not([size]):not([multiple]), .input-group-sm > select.form-control:not([size]):not([multiple]),\n.input-group-sm > .input-group-prepend > select.input-group-text:not([size]):not([multiple]),\n.input-group-sm > .input-group-append > select.input-group-text:not([size]):not([multiple]),\n.input-group-sm > .input-group-prepend > select.btn:not([size]):not([multiple]),\n.input-group-sm > .input-group-append > select.btn:not([size]):not([multiple]) {\n height: calc(1.8125rem + 2px);\n}\n\n.form-control-lg, .input-group-lg > .form-control,\n.input-group-lg > .input-group-prepend > .input-group-text,\n.input-group-lg > .input-group-append > .input-group-text,\n.input-group-lg > .input-group-prepend > .btn,\n.input-group-lg > .input-group-append > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\nselect.form-control-lg:not([size]):not([multiple]), .input-group-lg > select.form-control:not([size]):not([multiple]),\n.input-group-lg > .input-group-prepend > select.input-group-text:not([size]):not([multiple]),\n.input-group-lg > .input-group-append > select.input-group-text:not([size]):not([multiple]),\n.input-group-lg > .input-group-prepend > select.btn:not([size]):not([multiple]),\n.input-group-lg > .input-group-append > select.btn:not([size]):not([multiple]) {\n height: calc(2.875rem + 2px);\n}\n\n.form-group {\n margin-bottom: 1rem;\n}\n\n.form-text {\n display: block;\n margin-top: 0.25rem;\n}\n\n.form-row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -5px;\n margin-left: -5px;\n}\n\n.form-row > .col,\n.form-row > [class*=\"col-\"] {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.form-check {\n position: relative;\n display: block;\n padding-left: 1.25rem;\n}\n\n.form-check-input {\n position: absolute;\n margin-top: 0.3rem;\n margin-left: -1.25rem;\n}\n\n.form-check-input:disabled ~ .form-check-label {\n color: #6c757d;\n}\n\n.form-check-label {\n margin-bottom: 0;\n}\n\n.form-check-inline {\n display: inline-flex;\n align-items: center;\n padding-left: 0;\n margin-right: 0.75rem;\n}\n\n.form-check-inline .form-check-input {\n position: static;\n margin-top: 0;\n margin-right: 0.3125rem;\n margin-left: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #28a745;\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: .5rem;\n margin-top: .1rem;\n font-size: .875rem;\n line-height: 1;\n color: #fff;\n background-color: rgba(40, 167, 69, 0.8);\n border-radius: .2rem;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid, .was-validated\n.custom-select:valid,\n.custom-select.is-valid {\n border-color: #28a745;\n}\n\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus, .was-validated\n.custom-select:valid:focus,\n.custom-select.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .form-control:valid ~ .valid-feedback,\n.was-validated .form-control:valid ~ .valid-tooltip, .form-control.is-valid ~ .valid-feedback,\n.form-control.is-valid ~ .valid-tooltip, .was-validated\n.custom-select:valid ~ .valid-feedback,\n.was-validated\n.custom-select:valid ~ .valid-tooltip,\n.custom-select.is-valid ~ .valid-feedback,\n.custom-select.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control-file:valid ~ .valid-feedback,\n.was-validated .form-control-file:valid ~ .valid-tooltip, .form-control-file.is-valid ~ .valid-feedback,\n.form-control-file.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: #28a745;\n}\n\n.was-validated .form-check-input:valid ~ .valid-feedback,\n.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback,\n.form-check-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label {\n color: #28a745;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before {\n background-color: #71dd8a;\n}\n\n.was-validated .custom-control-input:valid ~ .valid-feedback,\n.was-validated .custom-control-input:valid ~ .valid-tooltip, .custom-control-input.is-valid ~ .valid-feedback,\n.custom-control-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before {\n background-color: #34ce57;\n}\n\n.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid ~ .custom-file-label::before, .custom-file-input.is-valid ~ .custom-file-label::before {\n border-color: inherit;\n}\n\n.was-validated .custom-file-input:valid ~ .valid-feedback,\n.was-validated .custom-file-input:valid ~ .valid-tooltip, .custom-file-input.is-valid ~ .valid-feedback,\n.custom-file-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #dc3545;\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: .5rem;\n margin-top: .1rem;\n font-size: .875rem;\n line-height: 1;\n color: #fff;\n background-color: rgba(220, 53, 69, 0.8);\n border-radius: .2rem;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid, .was-validated\n.custom-select:invalid,\n.custom-select.is-invalid {\n border-color: #dc3545;\n}\n\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus, .was-validated\n.custom-select:invalid:focus,\n.custom-select.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .form-control:invalid ~ .invalid-feedback,\n.was-validated .form-control:invalid ~ .invalid-tooltip, .form-control.is-invalid ~ .invalid-feedback,\n.form-control.is-invalid ~ .invalid-tooltip, .was-validated\n.custom-select:invalid ~ .invalid-feedback,\n.was-validated\n.custom-select:invalid ~ .invalid-tooltip,\n.custom-select.is-invalid ~ .invalid-feedback,\n.custom-select.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control-file:invalid ~ .invalid-feedback,\n.was-validated .form-control-file:invalid ~ .invalid-tooltip, .form-control-file.is-invalid ~ .invalid-feedback,\n.form-control-file.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: #dc3545;\n}\n\n.was-validated .form-check-input:invalid ~ .invalid-feedback,\n.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback,\n.form-check-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label {\n color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before {\n background-color: #efa2a9;\n}\n\n.was-validated .custom-control-input:invalid ~ .invalid-feedback,\n.was-validated .custom-control-input:invalid ~ .invalid-tooltip, .custom-control-input.is-invalid ~ .invalid-feedback,\n.custom-control-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before {\n background-color: #e4606d;\n}\n\n.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid ~ .custom-file-label::before, .custom-file-input.is-invalid ~ .custom-file-label::before {\n border-color: inherit;\n}\n\n.was-validated .custom-file-input:invalid ~ .invalid-feedback,\n.was-validated .custom-file-input:invalid ~ .invalid-tooltip, .custom-file-input.is-invalid ~ .invalid-feedback,\n.custom-file-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.form-inline {\n display: flex;\n flex-flow: row wrap;\n align-items: center;\n}\n\n.form-inline .form-check {\n width: 100%;\n}\n\n@media (min-width: 576px) {\n .form-inline label {\n display: flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 0;\n }\n .form-inline .form-group {\n display: flex;\n flex: 0 0 auto;\n flex-flow: row wrap;\n align-items: center;\n margin-bottom: 0;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-plaintext {\n display: inline-block;\n }\n .form-inline .input-group,\n .form-inline .custom-select {\n width: auto;\n }\n .form-inline .form-check {\n display: flex;\n align-items: center;\n justify-content: center;\n width: auto;\n padding-left: 0;\n }\n .form-inline .form-check-input {\n position: relative;\n margin-top: 0;\n margin-right: 0.25rem;\n margin-left: 0;\n }\n .form-inline .custom-control {\n align-items: center;\n justify-content: center;\n }\n .form-inline .custom-control-label {\n margin-bottom: 0;\n }\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n user-select: none;\n border: 1px solid transparent;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n line-height: 1.5;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n\n.btn:hover, .btn:focus {\n text-decoration: none;\n}\n\n.btn:focus, .btn.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.btn.disabled, .btn:disabled {\n opacity: 0.65;\n}\n\n.btn:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\n.btn:not(:disabled):not(.disabled):active, .btn:not(:disabled):not(.disabled).active {\n background-image: none;\n}\n\na.btn.disabled,\nfieldset:disabled a.btn {\n pointer-events: none;\n}\n\n.btn-primary {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:hover {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n}\n\n.btn-primary:focus, .btn-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-primary.disabled, .btn-primary:disabled {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active,\n.show > .btn-primary.dropdown-toggle {\n color: #fff;\n background-color: #0062cc;\n border-color: #005cbf;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-secondary {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:hover {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n}\n\n.btn-secondary:focus, .btn-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-secondary.disabled, .btn-secondary:disabled {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-secondary.dropdown-toggle {\n color: #fff;\n background-color: #545b62;\n border-color: #4e555b;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-success {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:hover {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n}\n\n.btn-success:focus, .btn-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-success.disabled, .btn-success:disabled {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active,\n.show > .btn-success.dropdown-toggle {\n color: #fff;\n background-color: #1e7e34;\n border-color: #1c7430;\n}\n\n.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-info {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:hover {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n}\n\n.btn-info:focus, .btn-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-info.disabled, .btn-info:disabled {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,\n.show > .btn-info.dropdown-toggle {\n color: #fff;\n background-color: #117a8b;\n border-color: #10707f;\n}\n\n.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-warning {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:hover {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n}\n\n.btn-warning:focus, .btn-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-warning.disabled, .btn-warning:disabled {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active,\n.show > .btn-warning.dropdown-toggle {\n color: #212529;\n background-color: #d39e00;\n border-color: #c69500;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-danger {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:hover {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n}\n\n.btn-danger:focus, .btn-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-danger.disabled, .btn-danger:disabled {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active,\n.show > .btn-danger.dropdown-toggle {\n color: #fff;\n background-color: #bd2130;\n border-color: #b21f2d;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-light {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:hover {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n}\n\n.btn-light:focus, .btn-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-light.disabled, .btn-light:disabled {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active,\n.show > .btn-light.dropdown-toggle {\n color: #212529;\n background-color: #dae0e5;\n border-color: #d3d9df;\n}\n\n.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-dark {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:hover {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n}\n\n.btn-dark:focus, .btn-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-dark.disabled, .btn-dark:disabled {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active,\n.show > .btn-dark.dropdown-toggle {\n color: #fff;\n background-color: #1d2124;\n border-color: #171a1d;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-outline-primary {\n color: #007bff;\n background-color: transparent;\n background-image: none;\n border-color: #007bff;\n}\n\n.btn-outline-primary:hover {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:focus, .btn-outline-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-primary.disabled, .btn-outline-primary:disabled {\n color: #007bff;\n background-color: transparent;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-primary.dropdown-toggle {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-secondary {\n color: #6c757d;\n background-color: transparent;\n background-image: none;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:hover {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:focus, .btn-outline-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-secondary.disabled, .btn-outline-secondary:disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-secondary.dropdown-toggle {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-success {\n color: #28a745;\n background-color: transparent;\n background-image: none;\n border-color: #28a745;\n}\n\n.btn-outline-success:hover {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:focus, .btn-outline-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-success.disabled, .btn-outline-success:disabled {\n color: #28a745;\n background-color: transparent;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active,\n.show > .btn-outline-success.dropdown-toggle {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-info {\n color: #17a2b8;\n background-color: transparent;\n background-image: none;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:hover {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:focus, .btn-outline-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-info.disabled, .btn-outline-info:disabled {\n color: #17a2b8;\n background-color: transparent;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,\n.show > .btn-outline-info.dropdown-toggle {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-warning {\n color: #ffc107;\n background-color: transparent;\n background-image: none;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:hover {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:focus, .btn-outline-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-warning.disabled, .btn-outline-warning:disabled {\n color: #ffc107;\n background-color: transparent;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active,\n.show > .btn-outline-warning.dropdown-toggle {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-danger {\n color: #dc3545;\n background-color: transparent;\n background-image: none;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:hover {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:focus, .btn-outline-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-danger.disabled, .btn-outline-danger:disabled {\n color: #dc3545;\n background-color: transparent;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active,\n.show > .btn-outline-danger.dropdown-toggle {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-light {\n color: #f8f9fa;\n background-color: transparent;\n background-image: none;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:hover {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:focus, .btn-outline-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-light.disabled, .btn-outline-light:disabled {\n color: #f8f9fa;\n background-color: transparent;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active,\n.show > .btn-outline-light.dropdown-toggle {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-dark {\n color: #343a40;\n background-color: transparent;\n background-image: none;\n border-color: #343a40;\n}\n\n.btn-outline-dark:hover {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:focus, .btn-outline-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-outline-dark.disabled, .btn-outline-dark:disabled {\n color: #343a40;\n background-color: transparent;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active,\n.show > .btn-outline-dark.dropdown-toggle {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-link {\n font-weight: 400;\n color: #007bff;\n background-color: transparent;\n}\n\n.btn-link:hover {\n color: #0056b3;\n text-decoration: underline;\n background-color: transparent;\n border-color: transparent;\n}\n\n.btn-link:focus, .btn-link.focus {\n text-decoration: underline;\n border-color: transparent;\n box-shadow: none;\n}\n\n.btn-link:disabled, .btn-link.disabled {\n color: #6c757d;\n pointer-events: none;\n}\n\n.btn-lg, .btn-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.btn-block {\n display: block;\n width: 100%;\n}\n\n.btn-block + .btn-block {\n margin-top: 0.5rem;\n}\n\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n\n.dropup,\n.dropright,\n.dropdown,\n.dropleft {\n position: relative;\n}\n\n.dropdown-toggle::after {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 10rem;\n padding: 0.5rem 0;\n margin: 0.125rem 0 0;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n\n.dropup .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: 0.125rem;\n}\n\n.dropup .dropdown-toggle::after {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-menu {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: 0.125rem;\n}\n\n.dropright .dropdown-toggle::after {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n\n.dropright .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropleft .dropdown-menu {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: 0.125rem;\n}\n\n.dropleft .dropdown-toggle::after {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n\n.dropleft .dropdown-toggle::after {\n display: none;\n}\n\n.dropleft .dropdown-toggle::before {\n display: inline-block;\n width: 0;\n height: 0;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n\n.dropleft .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-menu[x-placement^=\"top\"], .dropdown-menu[x-placement^=\"right\"], .dropdown-menu[x-placement^=\"bottom\"], .dropdown-menu[x-placement^=\"left\"] {\n right: auto;\n bottom: auto;\n}\n\n.dropdown-divider {\n height: 0;\n margin: 0.5rem 0;\n overflow: hidden;\n border-top: 1px solid #e9ecef;\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: 0.25rem 1.5rem;\n clear: both;\n font-weight: 400;\n color: #212529;\n text-align: inherit;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n}\n\n.dropdown-item:hover, .dropdown-item:focus {\n color: #16181b;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.dropdown-item.active, .dropdown-item:active {\n color: #fff;\n text-decoration: none;\n background-color: #007bff;\n}\n\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: 0.5rem 1.5rem;\n margin-bottom: 0;\n font-size: 0.875rem;\n color: #6c757d;\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: 0.25rem 1.5rem;\n color: #212529;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle;\n}\n\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n flex: 0 1 auto;\n}\n\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover {\n z-index: 1;\n}\n\n.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group,\n.btn-group-vertical .btn + .btn,\n.btn-group-vertical .btn + .btn-group,\n.btn-group-vertical .btn-group + .btn,\n.btn-group-vertical .btn-group + .btn-group {\n margin-left: -1px;\n}\n\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n}\n\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n\n.dropdown-toggle-split::after,\n.dropup .dropdown-toggle-split::after,\n.dropright .dropdown-toggle-split::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.btn-group-vertical .btn,\n.btn-group-vertical .btn-group {\n width: 100%;\n}\n\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.btn-group-toggle > .btn,\n.btn-group-toggle > .btn-group > .btn {\n margin-bottom: 0;\n}\n\n.btn-group-toggle > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn input[type=\"checkbox\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n width: 100%;\n}\n\n.input-group > .form-control,\n.input-group > .custom-select,\n.input-group > .custom-file {\n position: relative;\n flex: 1 1 auto;\n width: 1%;\n margin-bottom: 0;\n}\n\n.input-group > .form-control:focus,\n.input-group > .custom-select:focus,\n.input-group > .custom-file:focus {\n z-index: 3;\n}\n\n.input-group > .form-control + .form-control,\n.input-group > .form-control + .custom-select,\n.input-group > .form-control + .custom-file,\n.input-group > .custom-select + .form-control,\n.input-group > .custom-select + .custom-select,\n.input-group > .custom-select + .custom-file,\n.input-group > .custom-file + .form-control,\n.input-group > .custom-file + .custom-select,\n.input-group > .custom-file + .custom-file {\n margin-left: -1px;\n}\n\n.input-group > .form-control:not(:last-child),\n.input-group > .custom-select:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .form-control:not(:first-child),\n.input-group > .custom-select:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group > .custom-file {\n display: flex;\n align-items: center;\n}\n\n.input-group > .custom-file:not(:last-child) .custom-file-label,\n.input-group > .custom-file:not(:last-child) .custom-file-label::after {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .custom-file:not(:first-child) .custom-file-label {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group-prepend,\n.input-group-append {\n display: flex;\n}\n\n.input-group-prepend .btn,\n.input-group-append .btn {\n position: relative;\n z-index: 2;\n}\n\n.input-group-prepend .btn + .btn,\n.input-group-prepend .btn + .input-group-text,\n.input-group-prepend .input-group-text + .input-group-text,\n.input-group-prepend .input-group-text + .btn,\n.input-group-append .btn + .btn,\n.input-group-append .btn + .input-group-text,\n.input-group-append .input-group-text + .input-group-text,\n.input-group-append .input-group-text + .btn {\n margin-left: -1px;\n}\n\n.input-group-prepend {\n margin-right: -1px;\n}\n\n.input-group-append {\n margin-left: -1px;\n}\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: 0.375rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n text-align: center;\n white-space: nowrap;\n background-color: #e9ecef;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.input-group-text input[type=\"radio\"],\n.input-group-text input[type=\"checkbox\"] {\n margin-top: 0;\n}\n\n.input-group > .input-group-prepend > .btn,\n.input-group > .input-group-prepend > .input-group-text,\n.input-group > .input-group-append:not(:last-child) > .btn,\n.input-group > .input-group-append:not(:last-child) > .input-group-text,\n.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .input-group-append > .btn,\n.input-group > .input-group-append > .input-group-text,\n.input-group > .input-group-prepend:not(:first-child) > .btn,\n.input-group > .input-group-prepend:not(:first-child) > .input-group-text,\n.input-group > .input-group-prepend:first-child > .btn:not(:first-child),\n.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.custom-control {\n position: relative;\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5rem;\n}\n\n.custom-control-inline {\n display: inline-flex;\n margin-right: 1rem;\n}\n\n.custom-control-input {\n position: absolute;\n z-index: -1;\n opacity: 0;\n}\n\n.custom-control-input:checked ~ .custom-control-label::before {\n color: #fff;\n background-color: #007bff;\n}\n\n.custom-control-input:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-control-input:active ~ .custom-control-label::before {\n color: #fff;\n background-color: #b3d7ff;\n}\n\n.custom-control-input:disabled ~ .custom-control-label {\n color: #6c757d;\n}\n\n.custom-control-input:disabled ~ .custom-control-label::before {\n background-color: #e9ecef;\n}\n\n.custom-control-label {\n position: relative;\n margin-bottom: 0;\n}\n\n.custom-control-label::before {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n pointer-events: none;\n content: \"\";\n user-select: none;\n background-color: #dee2e6;\n}\n\n.custom-control-label::after {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n content: \"\";\n background-repeat: no-repeat;\n background-position: center center;\n background-size: 50% 50%;\n}\n\n.custom-checkbox .custom-control-label::before {\n border-radius: 0.25rem;\n}\n\n.custom-checkbox .custom-control-input:checked ~ .custom-control-label::before {\n background-color: #007bff;\n}\n\n.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E\");\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {\n background-color: #007bff;\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E\");\n}\n\n.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-radio .custom-control-label::before {\n border-radius: 50%;\n}\n\n.custom-radio .custom-control-input:checked ~ .custom-control-label::before {\n background-color: #007bff;\n}\n\n.custom-radio .custom-control-input:checked ~ .custom-control-label::after {\n background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E\");\n}\n\n.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-select {\n display: inline-block;\n width: 100%;\n height: calc(2.25rem + 2px);\n padding: 0.375rem 1.75rem 0.375rem 0.75rem;\n line-height: 1.5;\n color: #495057;\n vertical-align: middle;\n background: #fff url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E\") no-repeat right 0.75rem center;\n background-size: 8px 10px;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n appearance: none;\n}\n\n.custom-select:focus {\n border-color: #80bdff;\n outline: 0;\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.075), 0 0 5px rgba(128, 189, 255, 0.5);\n}\n\n.custom-select:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.custom-select[multiple], .custom-select[size]:not([size=\"1\"]) {\n height: auto;\n padding-right: 0.75rem;\n background-image: none;\n}\n\n.custom-select:disabled {\n color: #6c757d;\n background-color: #e9ecef;\n}\n\n.custom-select::-ms-expand {\n opacity: 0;\n}\n\n.custom-select-sm {\n height: calc(1.8125rem + 2px);\n padding-top: 0.375rem;\n padding-bottom: 0.375rem;\n font-size: 75%;\n}\n\n.custom-select-lg {\n height: calc(2.875rem + 2px);\n padding-top: 0.375rem;\n padding-bottom: 0.375rem;\n font-size: 125%;\n}\n\n.custom-file {\n position: relative;\n display: inline-block;\n width: 100%;\n height: calc(2.25rem + 2px);\n margin-bottom: 0;\n}\n\n.custom-file-input {\n position: relative;\n z-index: 2;\n width: 100%;\n height: calc(2.25rem + 2px);\n margin: 0;\n opacity: 0;\n}\n\n.custom-file-input:focus ~ .custom-file-label {\n border-color: #80bdff;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-file-input:focus ~ .custom-file-label::after {\n border-color: #80bdff;\n}\n\n.custom-file-input:lang(en) ~ .custom-file-label::after {\n content: \"Browse\";\n}\n\n.custom-file-label {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1;\n height: calc(2.25rem + 2px);\n padding: 0.375rem 0.75rem;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.custom-file-label::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 3;\n display: block;\n height: 2.25rem;\n padding: 0.375rem 0.75rem;\n line-height: 1.5;\n color: #495057;\n content: \"Browse\";\n background-color: #e9ecef;\n border-left: 1px solid #ced4da;\n border-radius: 0 0.25rem 0.25rem 0;\n}\n\n.custom-range {\n width: 100%;\n padding-left: 0;\n background-color: transparent;\n appearance: none;\n}\n\n.custom-range:focus {\n outline: none;\n}\n\n.custom-range::-moz-focus-outer {\n border: 0;\n}\n\n.custom-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n appearance: none;\n}\n\n.custom-range::-webkit-slider-thumb:focus {\n outline: none;\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range::-webkit-slider-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n appearance: none;\n}\n\n.custom-range::-moz-range-thumb:focus {\n outline: none;\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range::-moz-range-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n appearance: none;\n}\n\n.custom-range::-ms-thumb:focus {\n outline: none;\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range::-ms-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-ms-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: transparent;\n border-color: transparent;\n border-width: 0.5rem;\n}\n\n.custom-range::-ms-fill-lower {\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-fill-upper {\n margin-right: 15px;\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.nav {\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: 0.5rem 1rem;\n}\n\n.nav-link:hover, .nav-link:focus {\n text-decoration: none;\n}\n\n.nav-link.disabled {\n color: #6c757d;\n}\n\n.nav-tabs {\n border-bottom: 1px solid #dee2e6;\n}\n\n.nav-tabs .nav-item {\n margin-bottom: -1px;\n}\n\n.nav-tabs .nav-link {\n border: 1px solid transparent;\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n border-color: #e9ecef #e9ecef #dee2e6;\n}\n\n.nav-tabs .nav-link.disabled {\n color: #6c757d;\n background-color: transparent;\n border-color: transparent;\n}\n\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: #495057;\n background-color: #fff;\n border-color: #dee2e6 #dee2e6 #fff;\n}\n\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills .nav-link {\n border-radius: 0.25rem;\n}\n\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: #fff;\n background-color: #007bff;\n}\n\n.nav-fill .nav-item {\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified .nav-item {\n flex-basis: 0;\n flex-grow: 1;\n text-align: center;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 1rem;\n}\n\n.navbar > .container,\n.navbar > .container-fluid {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n}\n\n.navbar-brand {\n display: inline-block;\n padding-top: 0.3125rem;\n padding-bottom: 0.3125rem;\n margin-right: 1rem;\n font-size: 1.25rem;\n line-height: inherit;\n white-space: nowrap;\n}\n\n.navbar-brand:hover, .navbar-brand:focus {\n text-decoration: none;\n}\n\n.navbar-nav {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.navbar-nav .nav-link {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-nav .dropdown-menu {\n position: static;\n float: none;\n}\n\n.navbar-text {\n display: inline-block;\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.navbar-collapse {\n flex-basis: 100%;\n flex-grow: 1;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: 0.25rem 0.75rem;\n font-size: 1.25rem;\n line-height: 1;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.navbar-toggler:hover, .navbar-toggler:focus {\n text-decoration: none;\n}\n\n.navbar-toggler:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n content: \"\";\n background: no-repeat center center;\n background-size: 100% 100%;\n}\n\n@media (max-width: 575.98px) {\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-sm > .container,\n .navbar-expand-sm > .container-fluid {\n flex-wrap: nowrap;\n }\n .navbar-expand-sm .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 767.98px) {\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 768px) {\n .navbar-expand-md {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-md > .container,\n .navbar-expand-md > .container-fluid {\n flex-wrap: nowrap;\n }\n .navbar-expand-md .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 991.98px) {\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 992px) {\n .navbar-expand-lg {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-lg > .container,\n .navbar-expand-lg > .container-fluid {\n flex-wrap: nowrap;\n }\n .navbar-expand-lg .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 1199.98px) {\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xl > .container,\n .navbar-expand-xl > .container-fluid {\n flex-wrap: nowrap;\n }\n .navbar-expand-xl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n}\n\n.navbar-expand {\n flex-flow: row nowrap;\n justify-content: flex-start;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-expand .navbar-nav {\n flex-direction: row;\n}\n\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n\n.navbar-expand .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid {\n flex-wrap: nowrap;\n}\n\n.navbar-expand .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n}\n\n.navbar-expand .navbar-toggler {\n display: none;\n}\n\n.navbar-light .navbar-brand {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-nav .nav-link {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {\n color: rgba(0, 0, 0, 0.7);\n}\n\n.navbar-light .navbar-nav .nav-link.disabled {\n color: rgba(0, 0, 0, 0.3);\n}\n\n.navbar-light .navbar-nav .show > .nav-link,\n.navbar-light .navbar-nav .active > .nav-link,\n.navbar-light .navbar-nav .nav-link.show,\n.navbar-light .navbar-nav .nav-link.active {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-toggler {\n color: rgba(0, 0, 0, 0.5);\n border-color: rgba(0, 0, 0, 0.1);\n}\n\n.navbar-light .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E\");\n}\n\n.navbar-light .navbar-text {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-text a {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-dark .navbar-brand {\n color: #fff;\n}\n\n.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {\n color: #fff;\n}\n\n.navbar-dark .navbar-nav .nav-link {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {\n color: rgba(255, 255, 255, 0.75);\n}\n\n.navbar-dark .navbar-nav .nav-link.disabled {\n color: rgba(255, 255, 255, 0.25);\n}\n\n.navbar-dark .navbar-nav .show > .nav-link,\n.navbar-dark .navbar-nav .active > .nav-link,\n.navbar-dark .navbar-nav .nav-link.show,\n.navbar-dark .navbar-nav .nav-link.active {\n color: #fff;\n}\n\n.navbar-dark .navbar-toggler {\n color: rgba(255, 255, 255, 0.5);\n border-color: rgba(255, 255, 255, 0.1);\n}\n\n.navbar-dark .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E\");\n}\n\n.navbar-dark .navbar-text {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-text a {\n color: #fff;\n}\n\n.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus {\n color: #fff;\n}\n\n.card {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: border-box;\n border: 1px solid rgba(0, 0, 0, 0.125);\n border-radius: 0.25rem;\n}\n\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n\n.card > .list-group:first-child .list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.card > .list-group:last-child .list-group-item:last-child {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.card-body {\n flex: 1 1 auto;\n padding: 1.25rem;\n}\n\n.card-title {\n margin-bottom: 0.75rem;\n}\n\n.card-subtitle {\n margin-top: -0.375rem;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link:hover {\n text-decoration: none;\n}\n\n.card-link + .card-link {\n margin-left: 1.25rem;\n}\n\n.card-header {\n padding: 0.75rem 1.25rem;\n margin-bottom: 0;\n background-color: rgba(0, 0, 0, 0.03);\n border-bottom: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-header:first-child {\n border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;\n}\n\n.card-header + .list-group .list-group-item:first-child {\n border-top: 0;\n}\n\n.card-footer {\n padding: 0.75rem 1.25rem;\n background-color: rgba(0, 0, 0, 0.03);\n border-top: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-footer:last-child {\n border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);\n}\n\n.card-header-tabs {\n margin-right: -0.625rem;\n margin-bottom: -0.75rem;\n margin-left: -0.625rem;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -0.625rem;\n margin-left: -0.625rem;\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: 1.25rem;\n}\n\n.card-img {\n width: 100%;\n border-radius: calc(0.25rem - 1px);\n}\n\n.card-img-top {\n width: 100%;\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card-img-bottom {\n width: 100%;\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-deck {\n display: flex;\n flex-direction: column;\n}\n\n.card-deck .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-deck {\n flex-flow: row wrap;\n margin-right: -15px;\n margin-left: -15px;\n }\n .card-deck .card {\n display: flex;\n flex: 1 0 0%;\n flex-direction: column;\n margin-right: 15px;\n margin-bottom: 0;\n margin-left: 15px;\n }\n}\n\n.card-group {\n display: flex;\n flex-direction: column;\n}\n\n.card-group > .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-group {\n flex-flow: row wrap;\n }\n .card-group > .card {\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:first-child {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:first-child .card-img-top,\n .card-group > .card:first-child .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:first-child .card-img-bottom,\n .card-group > .card:first-child .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:last-child {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:last-child .card-img-top,\n .card-group > .card:last-child .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:last-child .card-img-bottom,\n .card-group > .card:last-child .card-footer {\n border-bottom-left-radius: 0;\n }\n .card-group > .card:only-child {\n border-radius: 0.25rem;\n }\n .card-group > .card:only-child .card-img-top,\n .card-group > .card:only-child .card-header {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n }\n .card-group > .card:only-child .card-img-bottom,\n .card-group > .card:only-child .card-footer {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n }\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) {\n border-radius: 0;\n }\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-top,\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-header,\n .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-footer {\n border-radius: 0;\n }\n}\n\n.card-columns .card {\n margin-bottom: 0.75rem;\n}\n\n@media (min-width: 576px) {\n .card-columns {\n column-count: 3;\n column-gap: 1.25rem;\n orphans: 1;\n widows: 1;\n }\n .card-columns .card {\n display: inline-block;\n width: 100%;\n }\n}\n\n.accordion .card:not(:first-of-type):not(:last-of-type) {\n border-bottom: 0;\n border-radius: 0;\n}\n\n.accordion .card:not(:first-of-type) .card-header:first-child {\n border-radius: 0;\n}\n\n.accordion .card:first-of-type {\n border-bottom: 0;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.accordion .card:last-of-type {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.breadcrumb {\n display: flex;\n flex-wrap: wrap;\n padding: 0.75rem 1rem;\n margin-bottom: 1rem;\n list-style: none;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: 0.5rem;\n}\n\n.breadcrumb-item + .breadcrumb-item::before {\n display: inline-block;\n padding-right: 0.5rem;\n color: #6c757d;\n content: \"/\";\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: underline;\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: none;\n}\n\n.breadcrumb-item.active {\n color: #6c757d;\n}\n\n.pagination {\n display: flex;\n padding-left: 0;\n list-style: none;\n border-radius: 0.25rem;\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: 0.5rem 0.75rem;\n margin-left: -1px;\n line-height: 1.25;\n color: #007bff;\n background-color: #fff;\n border: 1px solid #dee2e6;\n}\n\n.page-link:hover {\n z-index: 2;\n color: #0056b3;\n text-decoration: none;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.page-link:focus {\n z-index: 2;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.page-link:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\n.page-item:first-child .page-link {\n margin-left: 0;\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.page-item:last-child .page-link {\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n}\n\n.page-item.active .page-link {\n z-index: 1;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.page-item.disabled .page-link {\n color: #6c757d;\n pointer-events: none;\n cursor: auto;\n background-color: #fff;\n border-color: #dee2e6;\n}\n\n.pagination-lg .page-link {\n padding: 0.75rem 1.5rem;\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.pagination-lg .page-item:first-child .page-link {\n border-top-left-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n\n.pagination-lg .page-item:last-child .page-link {\n border-top-right-radius: 0.3rem;\n border-bottom-right-radius: 0.3rem;\n}\n\n.pagination-sm .page-link {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.pagination-sm .page-item:first-child .page-link {\n border-top-left-radius: 0.2rem;\n border-bottom-left-radius: 0.2rem;\n}\n\n.pagination-sm .page-item:last-child .page-link {\n border-top-right-radius: 0.2rem;\n border-bottom-right-radius: 0.2rem;\n}\n\n.badge {\n display: inline-block;\n padding: 0.25em 0.4em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25rem;\n}\n\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.badge-pill {\n padding-right: 0.6em;\n padding-left: 0.6em;\n border-radius: 10rem;\n}\n\n.badge-primary {\n color: #fff;\n background-color: #007bff;\n}\n\n.badge-primary[href]:hover, .badge-primary[href]:focus {\n color: #fff;\n text-decoration: none;\n background-color: #0062cc;\n}\n\n.badge-secondary {\n color: #fff;\n background-color: #6c757d;\n}\n\n.badge-secondary[href]:hover, .badge-secondary[href]:focus {\n color: #fff;\n text-decoration: none;\n background-color: #545b62;\n}\n\n.badge-success {\n color: #fff;\n background-color: #28a745;\n}\n\n.badge-success[href]:hover, .badge-success[href]:focus {\n color: #fff;\n text-decoration: none;\n background-color: #1e7e34;\n}\n\n.badge-info {\n color: #fff;\n background-color: #17a2b8;\n}\n\n.badge-info[href]:hover, .badge-info[href]:focus {\n color: #fff;\n text-decoration: none;\n background-color: #117a8b;\n}\n\n.badge-warning {\n color: #212529;\n background-color: #ffc107;\n}\n\n.badge-warning[href]:hover, .badge-warning[href]:focus {\n color: #212529;\n text-decoration: none;\n background-color: #d39e00;\n}\n\n.badge-danger {\n color: #fff;\n background-color: #dc3545;\n}\n\n.badge-danger[href]:hover, .badge-danger[href]:focus {\n color: #fff;\n text-decoration: none;\n background-color: #bd2130;\n}\n\n.badge-light {\n color: #212529;\n background-color: #f8f9fa;\n}\n\n.badge-light[href]:hover, .badge-light[href]:focus {\n color: #212529;\n text-decoration: none;\n background-color: #dae0e5;\n}\n\n.badge-dark {\n color: #fff;\n background-color: #343a40;\n}\n\n.badge-dark[href]:hover, .badge-dark[href]:focus {\n color: #fff;\n text-decoration: none;\n background-color: #1d2124;\n}\n\n.jumbotron {\n padding: 2rem 1rem;\n margin-bottom: 2rem;\n background-color: #e9ecef;\n border-radius: 0.3rem;\n}\n\n@media (min-width: 576px) {\n .jumbotron {\n padding: 4rem 2rem;\n }\n}\n\n.jumbotron-fluid {\n padding-right: 0;\n padding-left: 0;\n border-radius: 0;\n}\n\n.alert {\n position: relative;\n padding: 0.75rem 1.25rem;\n margin-bottom: 1rem;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n}\n\n.alert-dismissible {\n padding-right: 4rem;\n}\n\n.alert-dismissible .close {\n position: absolute;\n top: 0;\n right: 0;\n padding: 0.75rem 1.25rem;\n color: inherit;\n}\n\n.alert-primary {\n color: #004085;\n background-color: #cce5ff;\n border-color: #b8daff;\n}\n\n.alert-primary hr {\n border-top-color: #9fcdff;\n}\n\n.alert-primary .alert-link {\n color: #002752;\n}\n\n.alert-secondary {\n color: #383d41;\n background-color: #e2e3e5;\n border-color: #d6d8db;\n}\n\n.alert-secondary hr {\n border-top-color: #c8cbcf;\n}\n\n.alert-secondary .alert-link {\n color: #202326;\n}\n\n.alert-success {\n color: #155724;\n background-color: #d4edda;\n border-color: #c3e6cb;\n}\n\n.alert-success hr {\n border-top-color: #b1dfbb;\n}\n\n.alert-success .alert-link {\n color: #0b2e13;\n}\n\n.alert-info {\n color: #0c5460;\n background-color: #d1ecf1;\n border-color: #bee5eb;\n}\n\n.alert-info hr {\n border-top-color: #abdde5;\n}\n\n.alert-info .alert-link {\n color: #062c33;\n}\n\n.alert-warning {\n color: #856404;\n background-color: #fff3cd;\n border-color: #ffeeba;\n}\n\n.alert-warning hr {\n border-top-color: #ffe8a1;\n}\n\n.alert-warning .alert-link {\n color: #533f03;\n}\n\n.alert-danger {\n color: #721c24;\n background-color: #f8d7da;\n border-color: #f5c6cb;\n}\n\n.alert-danger hr {\n border-top-color: #f1b0b7;\n}\n\n.alert-danger .alert-link {\n color: #491217;\n}\n\n.alert-light {\n color: #818182;\n background-color: #fefefe;\n border-color: #fdfdfe;\n}\n\n.alert-light hr {\n border-top-color: #ececf6;\n}\n\n.alert-light .alert-link {\n color: #686868;\n}\n\n.alert-dark {\n color: #1b1e21;\n background-color: #d6d8d9;\n border-color: #c6c8ca;\n}\n\n.alert-dark hr {\n border-top-color: #b9bbbe;\n}\n\n.alert-dark .alert-link {\n color: #040505;\n}\n\n@keyframes progress-bar-stripes {\n from {\n background-position: 1rem 0;\n }\n to {\n background-position: 0 0;\n }\n}\n\n.progress {\n display: flex;\n height: 1rem;\n overflow: hidden;\n font-size: 0.75rem;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n background-color: #007bff;\n transition: width 0.6s ease;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 1rem 1rem;\n}\n\n.progress-bar-animated {\n animation: progress-bar-stripes 1s linear infinite;\n}\n\n.media {\n display: flex;\n align-items: flex-start;\n}\n\n.media-body {\n flex: 1;\n}\n\n.list-group {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n}\n\n.list-group-item-action {\n width: 100%;\n color: #495057;\n text-align: inherit;\n}\n\n.list-group-item-action:hover, .list-group-item-action:focus {\n color: #495057;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.list-group-item-action:active {\n color: #212529;\n background-color: #e9ecef;\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 0.75rem 1.25rem;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.list-group-item:first-child {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.list-group-item:hover, .list-group-item:focus {\n z-index: 1;\n text-decoration: none;\n}\n\n.list-group-item.disabled, .list-group-item:disabled {\n color: #6c757d;\n background-color: #fff;\n}\n\n.list-group-item.active {\n z-index: 2;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.list-group-flush .list-group-item {\n border-right: 0;\n border-left: 0;\n border-radius: 0;\n}\n\n.list-group-flush:first-child .list-group-item:first-child {\n border-top: 0;\n}\n\n.list-group-flush:last-child .list-group-item:last-child {\n border-bottom: 0;\n}\n\n.list-group-item-primary {\n color: #004085;\n background-color: #b8daff;\n}\n\n.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {\n color: #004085;\n background-color: #9fcdff;\n}\n\n.list-group-item-primary.list-group-item-action.active {\n color: #fff;\n background-color: #004085;\n border-color: #004085;\n}\n\n.list-group-item-secondary {\n color: #383d41;\n background-color: #d6d8db;\n}\n\n.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {\n color: #383d41;\n background-color: #c8cbcf;\n}\n\n.list-group-item-secondary.list-group-item-action.active {\n color: #fff;\n background-color: #383d41;\n border-color: #383d41;\n}\n\n.list-group-item-success {\n color: #155724;\n background-color: #c3e6cb;\n}\n\n.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {\n color: #155724;\n background-color: #b1dfbb;\n}\n\n.list-group-item-success.list-group-item-action.active {\n color: #fff;\n background-color: #155724;\n border-color: #155724;\n}\n\n.list-group-item-info {\n color: #0c5460;\n background-color: #bee5eb;\n}\n\n.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {\n color: #0c5460;\n background-color: #abdde5;\n}\n\n.list-group-item-info.list-group-item-action.active {\n color: #fff;\n background-color: #0c5460;\n border-color: #0c5460;\n}\n\n.list-group-item-warning {\n color: #856404;\n background-color: #ffeeba;\n}\n\n.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {\n color: #856404;\n background-color: #ffe8a1;\n}\n\n.list-group-item-warning.list-group-item-action.active {\n color: #fff;\n background-color: #856404;\n border-color: #856404;\n}\n\n.list-group-item-danger {\n color: #721c24;\n background-color: #f5c6cb;\n}\n\n.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {\n color: #721c24;\n background-color: #f1b0b7;\n}\n\n.list-group-item-danger.list-group-item-action.active {\n color: #fff;\n background-color: #721c24;\n border-color: #721c24;\n}\n\n.list-group-item-light {\n color: #818182;\n background-color: #fdfdfe;\n}\n\n.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {\n color: #818182;\n background-color: #ececf6;\n}\n\n.list-group-item-light.list-group-item-action.active {\n color: #fff;\n background-color: #818182;\n border-color: #818182;\n}\n\n.list-group-item-dark {\n color: #1b1e21;\n background-color: #c6c8ca;\n}\n\n.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {\n color: #1b1e21;\n background-color: #b9bbbe;\n}\n\n.list-group-item-dark.list-group-item-action.active {\n color: #fff;\n background-color: #1b1e21;\n border-color: #1b1e21;\n}\n\n.close {\n float: right;\n font-size: 1.5rem;\n font-weight: 700;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n opacity: .5;\n}\n\n.close:hover, .close:focus {\n color: #000;\n text-decoration: none;\n opacity: .75;\n}\n\n.close:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\nbutton.close {\n padding: 0;\n background-color: transparent;\n border: 0;\n -webkit-appearance: none;\n}\n\n.modal-open {\n overflow: hidden;\n}\n\n.modal {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1050;\n display: none;\n overflow: hidden;\n outline: 0;\n}\n\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 0.5rem;\n pointer-events: none;\n}\n\n.modal.fade .modal-dialog {\n transition: transform 0.3s ease-out;\n transform: translate(0, -25%);\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n\n.modal.show .modal-dialog {\n transform: translate(0, 0);\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: calc(100% - (0.5rem * 2));\n}\n\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n pointer-events: auto;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n outline: 0;\n}\n\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n background-color: #000;\n}\n\n.modal-backdrop.fade {\n opacity: 0;\n}\n\n.modal-backdrop.show {\n opacity: 0.5;\n}\n\n.modal-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding: 1rem;\n border-bottom: 1px solid #e9ecef;\n border-top-left-radius: 0.3rem;\n border-top-right-radius: 0.3rem;\n}\n\n.modal-header .close {\n padding: 1rem;\n margin: -1rem -1rem -1rem auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.modal-body {\n position: relative;\n flex: 1 1 auto;\n padding: 1rem;\n}\n\n.modal-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n padding: 1rem;\n border-top: 1px solid #e9ecef;\n}\n\n.modal-footer > :not(:first-child) {\n margin-left: .25rem;\n}\n\n.modal-footer > :not(:last-child) {\n margin-right: .25rem;\n}\n\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n\n@media (min-width: 576px) {\n .modal-dialog {\n max-width: 500px;\n margin: 1.75rem auto;\n }\n .modal-dialog-centered {\n min-height: calc(100% - (1.75rem * 2));\n }\n .modal-sm {\n max-width: 300px;\n }\n}\n\n@media (min-width: 992px) {\n .modal-lg {\n max-width: 800px;\n }\n}\n\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n opacity: 0;\n}\n\n.tooltip.show {\n opacity: 0.9;\n}\n\n.tooltip .arrow {\n position: absolute;\n display: block;\n width: 0.8rem;\n height: 0.4rem;\n}\n\n.tooltip .arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top, .bs-tooltip-auto[x-placement^=\"top\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^=\"top\"] .arrow {\n bottom: 0;\n}\n\n.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^=\"top\"] .arrow::before {\n top: 0;\n border-width: 0.4rem 0.4rem 0;\n border-top-color: #000;\n}\n\n.bs-tooltip-right, .bs-tooltip-auto[x-placement^=\"right\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^=\"right\"] .arrow {\n left: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^=\"right\"] .arrow::before {\n right: 0;\n border-width: 0.4rem 0.4rem 0.4rem 0;\n border-right-color: #000;\n}\n\n.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^=\"bottom\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow {\n top: 0;\n}\n\n.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow::before {\n bottom: 0;\n border-width: 0 0.4rem 0.4rem;\n border-bottom-color: #000;\n}\n\n.bs-tooltip-left, .bs-tooltip-auto[x-placement^=\"left\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^=\"left\"] .arrow {\n right: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^=\"left\"] .arrow::before {\n left: 0;\n border-width: 0.4rem 0 0.4rem 0.4rem;\n border-left-color: #000;\n}\n\n.tooltip-inner {\n max-width: 200px;\n padding: 0.25rem 0.5rem;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 0.25rem;\n}\n\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: block;\n max-width: 276px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n}\n\n.popover .arrow {\n position: absolute;\n display: block;\n width: 1rem;\n height: 0.5rem;\n margin: 0 0.3rem;\n}\n\n.popover .arrow::before, .popover .arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-popover-top, .bs-popover-auto[x-placement^=\"top\"] {\n margin-bottom: 0.5rem;\n}\n\n.bs-popover-top .arrow, .bs-popover-auto[x-placement^=\"top\"] .arrow {\n bottom: calc((0.5rem + 1px) * -1);\n}\n\n.bs-popover-top .arrow::before, .bs-popover-auto[x-placement^=\"top\"] .arrow::before,\n.bs-popover-top .arrow::after,\n.bs-popover-auto[x-placement^=\"top\"] .arrow::after {\n border-width: 0.5rem 0.5rem 0;\n}\n\n.bs-popover-top .arrow::before, .bs-popover-auto[x-placement^=\"top\"] .arrow::before {\n bottom: 0;\n border-top-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-top .arrow::after,\n.bs-popover-auto[x-placement^=\"top\"] .arrow::after {\n bottom: 1px;\n border-top-color: #fff;\n}\n\n.bs-popover-right, .bs-popover-auto[x-placement^=\"right\"] {\n margin-left: 0.5rem;\n}\n\n.bs-popover-right .arrow, .bs-popover-auto[x-placement^=\"right\"] .arrow {\n left: calc((0.5rem + 1px) * -1);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-right .arrow::before, .bs-popover-auto[x-placement^=\"right\"] .arrow::before,\n.bs-popover-right .arrow::after,\n.bs-popover-auto[x-placement^=\"right\"] .arrow::after {\n border-width: 0.5rem 0.5rem 0.5rem 0;\n}\n\n.bs-popover-right .arrow::before, .bs-popover-auto[x-placement^=\"right\"] .arrow::before {\n left: 0;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-right .arrow::after,\n.bs-popover-auto[x-placement^=\"right\"] .arrow::after {\n left: 1px;\n border-right-color: #fff;\n}\n\n.bs-popover-bottom, .bs-popover-auto[x-placement^=\"bottom\"] {\n margin-top: 0.5rem;\n}\n\n.bs-popover-bottom .arrow, .bs-popover-auto[x-placement^=\"bottom\"] .arrow {\n top: calc((0.5rem + 1px) * -1);\n}\n\n.bs-popover-bottom .arrow::before, .bs-popover-auto[x-placement^=\"bottom\"] .arrow::before,\n.bs-popover-bottom .arrow::after,\n.bs-popover-auto[x-placement^=\"bottom\"] .arrow::after {\n border-width: 0 0.5rem 0.5rem 0.5rem;\n}\n\n.bs-popover-bottom .arrow::before, .bs-popover-auto[x-placement^=\"bottom\"] .arrow::before {\n top: 0;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-bottom .arrow::after,\n.bs-popover-auto[x-placement^=\"bottom\"] .arrow::after {\n top: 1px;\n border-bottom-color: #fff;\n}\n\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^=\"bottom\"] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: 1rem;\n margin-left: -0.5rem;\n content: \"\";\n border-bottom: 1px solid #f7f7f7;\n}\n\n.bs-popover-left, .bs-popover-auto[x-placement^=\"left\"] {\n margin-right: 0.5rem;\n}\n\n.bs-popover-left .arrow, .bs-popover-auto[x-placement^=\"left\"] .arrow {\n right: calc((0.5rem + 1px) * -1);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-left .arrow::before, .bs-popover-auto[x-placement^=\"left\"] .arrow::before,\n.bs-popover-left .arrow::after,\n.bs-popover-auto[x-placement^=\"left\"] .arrow::after {\n border-width: 0.5rem 0 0.5rem 0.5rem;\n}\n\n.bs-popover-left .arrow::before, .bs-popover-auto[x-placement^=\"left\"] .arrow::before {\n right: 0;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n\n\n.bs-popover-left .arrow::after,\n.bs-popover-auto[x-placement^=\"left\"] .arrow::after {\n right: 1px;\n border-left-color: #fff;\n}\n\n.popover-header {\n padding: 0.5rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n color: inherit;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: 0.5rem 0.75rem;\n color: #212529;\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.carousel-item {\n position: relative;\n display: none;\n align-items: center;\n width: 100%;\n transition: transform 0.6s ease;\n backface-visibility: hidden;\n perspective: 1000px;\n}\n\n@media screen and (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next,\n.carousel-item-prev {\n position: absolute;\n top: 0;\n}\n\n.carousel-item-next.carousel-item-left,\n.carousel-item-prev.carousel-item-right {\n transform: translateX(0);\n}\n\n@supports (transform-style: preserve-3d) {\n .carousel-item-next.carousel-item-left,\n .carousel-item-prev.carousel-item-right {\n transform: translate3d(0, 0, 0);\n }\n}\n\n.carousel-item-next,\n.active.carousel-item-right {\n transform: translateX(100%);\n}\n\n@supports (transform-style: preserve-3d) {\n .carousel-item-next,\n .active.carousel-item-right {\n transform: translate3d(100%, 0, 0);\n }\n}\n\n.carousel-item-prev,\n.active.carousel-item-left {\n transform: translateX(-100%);\n}\n\n@supports (transform-style: preserve-3d) {\n .carousel-item-prev,\n .active.carousel-item-left {\n transform: translate3d(-100%, 0, 0);\n }\n}\n\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-duration: .6s;\n transition-property: opacity;\n}\n\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-left,\n.carousel-fade .carousel-item-prev.carousel-item-right {\n opacity: 1;\n}\n\n.carousel-fade .active.carousel-item-left,\n.carousel-fade .active.carousel-item-right {\n opacity: 0;\n}\n\n.carousel-fade .carousel-item-next,\n.carousel-fade .carousel-item-prev,\n.carousel-fade .carousel-item.active,\n.carousel-fade .active.carousel-item-left,\n.carousel-fade .active.carousel-item-prev {\n transform: translateX(0);\n}\n\n@supports (transform-style: preserve-3d) {\n .carousel-fade .carousel-item-next,\n .carousel-fade .carousel-item-prev,\n .carousel-fade .carousel-item.active,\n .carousel-fade .active.carousel-item-left,\n .carousel-fade .active.carousel-item-prev {\n transform: translate3d(0, 0, 0);\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 15%;\n color: #fff;\n text-align: center;\n opacity: 0.5;\n}\n\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: .9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 20px;\n height: 20px;\n background: transparent no-repeat center center;\n background-size: 100% 100%;\n}\n\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E\");\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E\");\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 10px;\n left: 0;\n z-index: 15;\n display: flex;\n justify-content: center;\n padding-left: 0;\n margin-right: 15%;\n margin-left: 15%;\n list-style: none;\n}\n\n.carousel-indicators li {\n position: relative;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: rgba(255, 255, 255, 0.5);\n}\n\n.carousel-indicators li::before {\n position: absolute;\n top: -10px;\n left: 0;\n display: inline-block;\n width: 100%;\n height: 10px;\n content: \"\";\n}\n\n.carousel-indicators li::after {\n position: absolute;\n bottom: -10px;\n left: 0;\n display: inline-block;\n width: 100%;\n height: 10px;\n content: \"\";\n}\n\n.carousel-indicators .active {\n background-color: #fff;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.bg-primary {\n background-color: #007bff !important;\n}\n\na.bg-primary:hover, a.bg-primary:focus,\nbutton.bg-primary:hover,\nbutton.bg-primary:focus {\n background-color: #0062cc !important;\n}\n\n.bg-secondary {\n background-color: #6c757d !important;\n}\n\na.bg-secondary:hover, a.bg-secondary:focus,\nbutton.bg-secondary:hover,\nbutton.bg-secondary:focus {\n background-color: #545b62 !important;\n}\n\n.bg-success {\n background-color: #28a745 !important;\n}\n\na.bg-success:hover, a.bg-success:focus,\nbutton.bg-success:hover,\nbutton.bg-success:focus {\n background-color: #1e7e34 !important;\n}\n\n.bg-info {\n background-color: #17a2b8 !important;\n}\n\na.bg-info:hover, a.bg-info:focus,\nbutton.bg-info:hover,\nbutton.bg-info:focus {\n background-color: #117a8b !important;\n}\n\n.bg-warning {\n background-color: #ffc107 !important;\n}\n\na.bg-warning:hover, a.bg-warning:focus,\nbutton.bg-warning:hover,\nbutton.bg-warning:focus {\n background-color: #d39e00 !important;\n}\n\n.bg-danger {\n background-color: #dc3545 !important;\n}\n\na.bg-danger:hover, a.bg-danger:focus,\nbutton.bg-danger:hover,\nbutton.bg-danger:focus {\n background-color: #bd2130 !important;\n}\n\n.bg-light {\n background-color: #f8f9fa !important;\n}\n\na.bg-light:hover, a.bg-light:focus,\nbutton.bg-light:hover,\nbutton.bg-light:focus {\n background-color: #dae0e5 !important;\n}\n\n.bg-dark {\n background-color: #343a40 !important;\n}\n\na.bg-dark:hover, a.bg-dark:focus,\nbutton.bg-dark:hover,\nbutton.bg-dark:focus {\n background-color: #1d2124 !important;\n}\n\n.bg-white {\n background-color: #fff !important;\n}\n\n.bg-transparent {\n background-color: transparent !important;\n}\n\n.border {\n border: 1px solid #dee2e6 !important;\n}\n\n.border-top {\n border-top: 1px solid #dee2e6 !important;\n}\n\n.border-right {\n border-right: 1px solid #dee2e6 !important;\n}\n\n.border-bottom {\n border-bottom: 1px solid #dee2e6 !important;\n}\n\n.border-left {\n border-left: 1px solid #dee2e6 !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-right-0 {\n border-right: 0 !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-left-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n border-color: #007bff !important;\n}\n\n.border-secondary {\n border-color: #6c757d !important;\n}\n\n.border-success {\n border-color: #28a745 !important;\n}\n\n.border-info {\n border-color: #17a2b8 !important;\n}\n\n.border-warning {\n border-color: #ffc107 !important;\n}\n\n.border-danger {\n border-color: #dc3545 !important;\n}\n\n.border-light {\n border-color: #f8f9fa !important;\n}\n\n.border-dark {\n border-color: #343a40 !important;\n}\n\n.border-white {\n border-color: #fff !important;\n}\n\n.rounded {\n border-radius: 0.25rem !important;\n}\n\n.rounded-top {\n border-top-left-radius: 0.25rem !important;\n border-top-right-radius: 0.25rem !important;\n}\n\n.rounded-right {\n border-top-right-radius: 0.25rem !important;\n border-bottom-right-radius: 0.25rem !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-left {\n border-top-left-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n}\n\n.embed-responsive {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n}\n\n.embed-responsive::before {\n display: block;\n content: \"\";\n}\n\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n\n.embed-responsive-21by9::before {\n padding-top: 42.857143%;\n}\n\n.embed-responsive-16by9::before {\n padding-top: 56.25%;\n}\n\n.embed-responsive-4by3::before {\n padding-top: 75%;\n}\n\n.embed-responsive-1by1::before {\n padding-top: 100%;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n}\n\n.float-left {\n float: left !important;\n}\n\n.float-right {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-left {\n float: left !important;\n }\n .float-sm-right {\n float: right !important;\n }\n .float-sm-none {\n float: none !important;\n }\n}\n\n@media (min-width: 768px) {\n .float-md-left {\n float: left !important;\n }\n .float-md-right {\n float: right !important;\n }\n .float-md-none {\n float: none !important;\n }\n}\n\n@media (min-width: 992px) {\n .float-lg-left {\n float: left !important;\n }\n .float-lg-right {\n float: right !important;\n }\n .float-lg-none {\n float: none !important;\n }\n}\n\n@media (min-width: 1200px) {\n .float-xl-left {\n float: left !important;\n }\n .float-xl-right {\n float: right !important;\n }\n .float-xl-none {\n float: none !important;\n }\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: sticky !important;\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n@supports (position: sticky) {\n .sticky-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.sr-only-focusable:active, .sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n overflow: visible;\n clip: auto;\n white-space: normal;\n}\n\n.shadow-sm {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n}\n\n.shadow {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n.shadow-lg {\n box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n\n.text-monospace {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n.text-justify {\n text-align: justify !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.text-left {\n text-align: left !important;\n}\n\n.text-right {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n@media (min-width: 576px) {\n .text-sm-left {\n text-align: left !important;\n }\n .text-sm-right {\n text-align: right !important;\n }\n .text-sm-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 768px) {\n .text-md-left {\n text-align: left !important;\n }\n .text-md-right {\n text-align: right !important;\n }\n .text-md-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 992px) {\n .text-lg-left {\n text-align: left !important;\n }\n .text-lg-right {\n text-align: right !important;\n }\n .text-lg-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 1200px) {\n .text-xl-left {\n text-align: left !important;\n }\n .text-xl-right {\n text-align: right !important;\n }\n .text-xl-center {\n text-align: center !important;\n }\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.font-weight-light {\n font-weight: 300 !important;\n}\n\n.font-weight-normal {\n font-weight: 400 !important;\n}\n\n.font-weight-bold {\n font-weight: 700 !important;\n}\n\n.font-italic {\n font-style: italic !important;\n}\n\n.text-white {\n color: #fff !important;\n}\n\n.text-primary {\n color: #007bff !important;\n}\n\na.text-primary:hover, a.text-primary:focus {\n color: #0062cc !important;\n}\n\n.text-secondary {\n color: #6c757d !important;\n}\n\na.text-secondary:hover, a.text-secondary:focus {\n color: #545b62 !important;\n}\n\n.text-success {\n color: #28a745 !important;\n}\n\na.text-success:hover, a.text-success:focus {\n color: #1e7e34 !important;\n}\n\n.text-info {\n color: #17a2b8 !important;\n}\n\na.text-info:hover, a.text-info:focus {\n color: #117a8b !important;\n}\n\n.text-warning {\n color: #ffc107 !important;\n}\n\na.text-warning:hover, a.text-warning:focus {\n color: #d39e00 !important;\n}\n\n.text-danger {\n color: #dc3545 !important;\n}\n\na.text-danger:hover, a.text-danger:focus {\n color: #bd2130 !important;\n}\n\n.text-light {\n color: #f8f9fa !important;\n}\n\na.text-light:hover, a.text-light:focus {\n color: #dae0e5 !important;\n}\n\n.text-dark {\n color: #343a40 !important;\n}\n\na.text-dark:hover, a.text-dark:focus {\n color: #1d2124 !important;\n}\n\n.text-body {\n color: #212529 !important;\n}\n\n.text-muted {\n color: #6c757d !important;\n}\n\n.text-black-50 {\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n@media print {\n *,\n *::before,\n *::after {\n text-shadow: none !important;\n box-shadow: none !important;\n }\n a:not(.btn) {\n text-decoration: underline;\n }\n abbr[title]::after {\n content: \" (\" attr(title) \")\";\n }\n pre {\n white-space: pre-wrap !important;\n }\n pre,\n blockquote {\n border: 1px solid #adb5bd;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n @page {\n size: a3;\n }\n body {\n min-width: 992px !important;\n }\n .container {\n min-width: 992px !important;\n }\n .navbar {\n display: none;\n }\n .badge {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #dee2e6 !important;\n }\n .table-dark {\n color: inherit;\n }\n .table-dark th,\n .table-dark td,\n .table-dark thead th,\n .table-dark tbody + tbody {\n border-color: #dee2e6;\n }\n .table .thead-dark th {\n color: inherit;\n border-color: #dee2e6;\n }\n}\n\n/*# sourceMappingURL=bootstrap.css.map */","// Hover mixin and `$enable-hover-media-query` are deprecated.\n//\n// Origally added during our alphas and maintained during betas, this mixin was\n// designed to prevent `:hover` stickiness on iOS-an issue where hover styles\n// would persist after initial touch.\n//\n// For backward compatibility, we've kept these mixins and updated them to\n// always return their regular pseudo-classes instead of a shimmed media query.\n//\n// Issue: https://github.com/twbs/bootstrap/issues/25195\n\n@mixin hover {\n &:hover { @content; }\n}\n\n@mixin hover-focus {\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin plain-hover-focus {\n &,\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin hover-focus-active {\n &:hover,\n &:focus,\n &:active {\n @content;\n }\n}\n","// stylelint-disable declaration-no-important, selector-list-comma-newline-after\n\n//\n// Headings\n//\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1, .h1 { font-size: $h1-font-size; }\nh2, .h2 { font-size: $h2-font-size; }\nh3, .h3 { font-size: $h3-font-size; }\nh4, .h4 { font-size: $h4-font-size; }\nh5, .h5 { font-size: $h5-font-size; }\nh6, .h6 { font-size: $h6-font-size; }\n\n.lead {\n font-size: $lead-font-size;\n font-weight: $lead-font-weight;\n}\n\n// Type display classes\n.display-1 {\n font-size: $display1-size;\n font-weight: $display1-weight;\n line-height: $display-line-height;\n}\n.display-2 {\n font-size: $display2-size;\n font-weight: $display2-weight;\n line-height: $display-line-height;\n}\n.display-3 {\n font-size: $display3-size;\n font-weight: $display3-weight;\n line-height: $display-line-height;\n}\n.display-4 {\n font-size: $display4-size;\n font-weight: $display4-weight;\n line-height: $display-line-height;\n}\n\n\n//\n// Horizontal rules\n//\n\nhr {\n margin-top: $hr-margin-y;\n margin-bottom: $hr-margin-y;\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n}\n\n\n//\n// Emphasis\n//\n\nsmall,\n.small {\n font-size: $small-font-size;\n font-weight: $font-weight-normal;\n}\n\nmark,\n.mark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n//\n// Lists\n//\n\n.list-unstyled {\n @include list-unstyled;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n @include list-unstyled;\n}\n.list-inline-item {\n display: inline-block;\n\n &:not(:last-child) {\n margin-right: $list-inline-padding;\n }\n}\n\n\n//\n// Misc\n//\n\n// Builds on `abbr`\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n// Blockquotes\n.blockquote {\n margin-bottom: $spacer;\n font-size: $blockquote-font-size;\n}\n\n.blockquote-footer {\n display: block;\n font-size: 80%; // back to default font-size\n color: $blockquote-small-color;\n\n &::before {\n content: \"\\2014 \\00A0\"; // em dash, nbsp\n }\n}\n","// Lists\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n@mixin list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n","// Responsive images (ensure images don't scale beyond their parents)\n//\n// This is purposefully opt-in via an explicit class rather than being the default for all ``s.\n// We previously tried the \"images are responsive by default\" approach in Bootstrap v2,\n// and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps)\n// which weren't expecting the images within themselves to be involuntarily resized.\n// See also https://github.com/twbs/bootstrap/issues/18178\n.img-fluid {\n @include img-fluid;\n}\n\n\n// Image thumbnails\n.img-thumbnail {\n padding: $thumbnail-padding;\n background-color: $thumbnail-bg;\n border: $thumbnail-border-width solid $thumbnail-border-color;\n @include border-radius($thumbnail-border-radius);\n @include box-shadow($thumbnail-box-shadow);\n\n // Keep them at most 100% wide\n @include img-fluid;\n}\n\n//\n// Figures\n//\n\n.figure {\n // Ensures the caption's text aligns with the image.\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: ($spacer / 2);\n line-height: 1;\n}\n\n.figure-caption {\n font-size: $figure-caption-font-size;\n color: $figure-caption-color;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n\n@mixin img-fluid {\n // Part 1: Set a maximum relative to the parent\n max-width: 100%;\n // Part 2: Override the height to auto, otherwise images will be stretched\n // when setting a width and height attribute on the img element.\n height: auto;\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size.\n\n// stylelint-disable indentation, media-query-list-comma-newline-after\n@mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) {\n background-image: url($file-1x);\n\n // Autoprefixer takes care of adding -webkit-min-device-pixel-ratio and -o-min-device-pixel-ratio,\n // but doesn't convert dppx=>dpi.\n // There's no such thing as unprefixed min-device-pixel-ratio since it's nonstandard.\n // Compatibility info: https://caniuse.com/#feat=css-media-resolution\n @media only screen and (min-resolution: 192dpi), // IE9-11 don't support dppx\n only screen and (min-resolution: 2dppx) { // Standardized\n background-image: url($file-2x);\n background-size: $width-1x $height-1x;\n }\n}\n","// Single side border-radius\n\n@mixin border-radius($radius: $border-radius) {\n @if $enable-rounded {\n border-radius: $radius;\n }\n}\n\n@mixin border-top-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: $radius;\n border-top-right-radius: $radius;\n }\n}\n\n@mixin border-right-radius($radius) {\n @if $enable-rounded {\n border-top-right-radius: $radius;\n border-bottom-right-radius: $radius;\n }\n}\n\n@mixin border-bottom-radius($radius) {\n @if $enable-rounded {\n border-bottom-right-radius: $radius;\n border-bottom-left-radius: $radius;\n }\n}\n\n@mixin border-left-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: $radius;\n border-bottom-left-radius: $radius;\n }\n}\n","// Inline code\ncode {\n font-size: $code-font-size;\n color: $code-color;\n word-break: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n font-size: $kbd-font-size;\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n @include box-shadow($kbd-box-shadow);\n\n kbd {\n padding: 0;\n font-size: 100%;\n font-weight: $nested-kbd-font-weight;\n @include box-shadow(none);\n }\n}\n\n// Blocks of code\npre {\n display: block;\n font-size: $code-font-size;\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: $pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n .container {\n @include make-container();\n @include make-container-max-widths();\n }\n}\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but with 100% width for\n// fluid, full width layouts.\n\n@if $enable-grid-classes {\n .container-fluid {\n @include make-container();\n }\n}\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container() {\n width: 100%;\n padding-right: ($grid-gutter-width / 2);\n padding-left: ($grid-gutter-width / 2);\n margin-right: auto;\n margin-left: auto;\n}\n\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n}\n\n@mixin make-row() {\n display: flex;\n flex-wrap: wrap;\n margin-right: ($grid-gutter-width / -2);\n margin-left: ($grid-gutter-width / -2);\n}\n\n@mixin make-col-ready() {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n min-height: 1px; // Prevent collapsing\n padding-right: ($grid-gutter-width / 2);\n padding-left: ($grid-gutter-width / 2);\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: $size / $columns;\n margin-left: if($num == 0, 0, percentage($num));\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.02px\n// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - .02px, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash infront.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($name, $breakpoints) {\n @content;\n }\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n min-height: 1px; // Prevent columns from collapsing when empty\n padding-right: ($gutter / 2);\n padding-left: ($gutter / 2);\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n max-width: 100%;\n }\n .col#{$infix}-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: none; // Reset earlier grid tiers\n }\n\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n\n .order#{$infix}-first { order: -1; }\n\n .order#{$infix}-last { order: $columns + 1; }\n\n @for $i from 0 through $columns {\n .order#{$infix}-#{$i} { order: $i; }\n }\n\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n }\n}\n","//\n// Basic Bootstrap table\n//\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: $spacer;\n background-color: $table-bg; // Reset for nesting within parents with `background-color`.\n\n th,\n td {\n padding: $table-cell-padding;\n vertical-align: top;\n border-top: $table-border-width solid $table-border-color;\n }\n\n thead th {\n vertical-align: bottom;\n border-bottom: (2 * $table-border-width) solid $table-border-color;\n }\n\n tbody + tbody {\n border-top: (2 * $table-border-width) solid $table-border-color;\n }\n\n .table {\n background-color: $body-bg;\n }\n}\n\n\n//\n// Condensed table w/ half padding\n//\n\n.table-sm {\n th,\n td {\n padding: $table-cell-padding-sm;\n }\n}\n\n\n// Border versions\n//\n// Add or remove borders all around the table and between all the columns.\n\n.table-bordered {\n border: $table-border-width solid $table-border-color;\n\n th,\n td {\n border: $table-border-width solid $table-border-color;\n }\n\n thead {\n th,\n td {\n border-bottom-width: (2 * $table-border-width);\n }\n }\n}\n\n.table-borderless {\n th,\n td,\n thead th,\n tbody + tbody {\n border: 0;\n }\n}\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n tbody tr:nth-of-type(#{$table-striped-order}) {\n background-color: $table-accent-bg;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n tbody tr {\n @include hover {\n background-color: $table-hover-bg;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n@each $color, $value in $theme-colors {\n @include table-row-variant($color, theme-color-level($color, -9));\n}\n\n@include table-row-variant(active, $table-active-bg);\n\n\n// Dark styles\n//\n// Same table markup, but inverted color scheme: dark background and light text.\n\n// stylelint-disable-next-line no-duplicate-selectors\n.table {\n .thead-dark {\n th {\n color: $table-dark-color;\n background-color: $table-dark-bg;\n border-color: $table-dark-border-color;\n }\n }\n\n .thead-light {\n th {\n color: $table-head-color;\n background-color: $table-head-bg;\n border-color: $table-border-color;\n }\n }\n}\n\n.table-dark {\n color: $table-dark-color;\n background-color: $table-dark-bg;\n\n th,\n td,\n thead th {\n border-color: $table-dark-border-color;\n }\n\n &.table-bordered {\n border: 0;\n }\n\n &.table-striped {\n tbody tr:nth-of-type(odd) {\n background-color: $table-dark-accent-bg;\n }\n }\n\n &.table-hover {\n tbody tr {\n @include hover {\n background-color: $table-dark-hover-bg;\n }\n }\n }\n}\n\n\n// Responsive tables\n//\n// Generate series of `.table-responsive-*` classes for configuring the screen\n// size of where your table will overflow.\n\n.table-responsive {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $next: breakpoint-next($breakpoint, $grid-breakpoints);\n $infix: breakpoint-infix($next, $grid-breakpoints);\n\n &#{$infix} {\n @include media-breakpoint-down($breakpoint) {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n -ms-overflow-style: -ms-autohiding-scrollbar; // See https://github.com/twbs/bootstrap/pull/10057\n\n // Prevent double border on horizontal scroll due to use of `display: block;`\n > .table-bordered {\n border: 0;\n }\n }\n }\n }\n}\n","// Tables\n\n@mixin table-row-variant($state, $background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table-#{$state} {\n &,\n > th,\n > td {\n background-color: $background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover {\n $hover-background: darken($background, 5%);\n\n .table-#{$state} {\n @include hover {\n background-color: $hover-background;\n\n > td,\n > th {\n background-color: $hover-background;\n }\n }\n }\n }\n}\n","// stylelint-disable selector-no-qualifying-type\n\n//\n// Textual form controls\n//\n\n.form-control {\n display: block;\n width: 100%;\n padding: $input-padding-y $input-padding-x;\n font-size: $font-size-base;\n line-height: $input-line-height;\n color: $input-color;\n background-color: $input-bg;\n background-clip: padding-box;\n border: $input-border-width solid $input-border-color;\n\n // Note: This has no effect on `s in CSS.\n @if $enable-rounded {\n // Manually use the if/else instead of the mixin to account for iOS override\n border-radius: $input-border-radius;\n } @else {\n // Otherwise undo the iOS default\n border-radius: 0;\n }\n\n @include box-shadow($input-box-shadow);\n @include transition($input-transition);\n\n // Unstyle the caret on ` receives focus\n // in IE and (under certain conditions) Edge, as it looks bad and cannot be made to\n // match the appearance of the native widget.\n // See https://github.com/twbs/bootstrap/issues/19398.\n color: $input-color;\n background-color: $input-bg;\n }\n}\n\n// Make file inputs better match text inputs by forcing them to new lines.\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n\n//\n// Labels\n//\n\n// For use with horizontal and inline forms, when you need the label (or legend)\n// text to align with the form controls.\n.col-form-label {\n padding-top: calc(#{$input-padding-y} + #{$input-border-width});\n padding-bottom: calc(#{$input-padding-y} + #{$input-border-width});\n margin-bottom: 0; // Override the `
- {% endfor %} + {% endfor %} - {% endblock %} {% macro progress(pg, class) %} @@ -67,5 +65,9 @@ {% else %} {% set type="success" %} {% endif %} - +
+
+
+
+
{% endmacro %} diff --git a/Resources/views/WebUI/show.html.twig b/Resources/views/WebUI/show.html.twig old mode 100644 new mode 100755 index 22bbe7f8..e4fddb2a --- a/Resources/views/WebUI/show.html.twig +++ b/Resources/views/WebUI/show.html.twig @@ -2,59 +2,46 @@ {% import _self as macro %} {% block body %} -
- - - -
- +
+
Locales:
+ {% for locale in locales %} + + {{ locale }} + + {% endfor %} +
Domains:
+ {% for domain in domains %} + + {{ domain }} + + {% endfor %} +
+
+

Translations {% if allow_create %} - Add new + Add new {% endif %}

{% if allow_create %} -
-
-
- - -
-
- - -
- -
-
-
+
+
+
+ + +
+
+ + +
+ +
+
+
{% endif %}
@@ -67,50 +54,59 @@ {{ macro.printMessage(idx + idxStart, message, allow_delete, file_base_path) }} {% endfor %}
+{% endblock %} +{% block javascripts %} + {{ parent() }} {% endblock %} {% macro printMessage(idx, message, allow_delete, base_path) %} -
-
- {% if message.new %} - - {% endif %} - {% if message.obsolete %} - - {% endif %} - {{ message.key }} - {% if allow_delete %} - - {% endif %} - -
- +
+
+
+
+
+
+ {% if message.new %} + + {% endif %} + {% if message.obsolete %} + + {% endif %} + {{ message.key }} + {% if allow_delete %} + + {% endif %} + +
+
-
- {% for locale,trans in message.otherTranslations if not trans is empty%} +
+ {% for locale,trans in message.otherTranslations if not trans is empty %} {{ locale }}: {{ trans }}
{% endfor %} -
-
+
{% if message.sourceLocations|length > 0 %} {% endif %}
+ {% endmacro %} From 57209e8fb604e02eb240c18d0c629d983f079e8b Mon Sep 17 00:00:00 2001 From: Yoh Kenn Date: Tue, 26 Jun 2018 19:53:59 +0800 Subject: [PATCH 047/234] Deprecated in Symfony 4.1 (#233) * Deprecated in Symfony 4.1 User Deprecated: Referencing controllers with TranslationBundle:EditInPlace:edit is deprecated since Symfony 4.1 * Deprecated in Symfony 4.1 User Deprecated: Referencing controllers * Deprecated in Symfony 4.1 User Deprecated: Referencing controllers --- Resources/config/routing_edit_in_place.yml | 2 +- Resources/config/routing_symfony_profiler.yml | 8 ++++---- Resources/config/routing_webui.yml | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Resources/config/routing_edit_in_place.yml b/Resources/config/routing_edit_in_place.yml index 1f0b3de7..830da64f 100644 --- a/Resources/config/routing_edit_in_place.yml +++ b/Resources/config/routing_edit_in_place.yml @@ -1,4 +1,4 @@ translation_edit_in_place_update: path: /_trans_edit_in_place/{configName}/{locale} methods: [POST] - defaults: { _controller: TranslationBundle:EditInPlace:edit } + defaults: { _controller: Translation\Bundle\Controller\EditInPlaceController::editAction } diff --git a/Resources/config/routing_symfony_profiler.yml b/Resources/config/routing_symfony_profiler.yml index 2eecf9e2..da5c999d 100644 --- a/Resources/config/routing_symfony_profiler.yml +++ b/Resources/config/routing_symfony_profiler.yml @@ -2,19 +2,19 @@ php_translation_profiler_translation_edit: path: /{token}/translation/edit methods: ["GET", "POST"] - defaults: { _controller: TranslationBundle:SymfonyProfiler:edit } + defaults: { _controller: Translation\Bundle\Controller\SymfonyProfilerController::editAction } php_translation_profiler_translation_sync: path: /{token}/translation/sync methods: ["POST"] - defaults: { _controller: TranslationBundle:SymfonyProfiler:sync } + defaults: { _controller: Translation\Bundle\Controller\SymfonyProfilerController::syncAction } php_translation_profiler_translation_sync_all: path: /{token}/translation/sync_all methods: ["POST"] - defaults: { _controller: TranslationBundle:SymfonyProfiler:syncAll } + defaults: { _controller: Translation\Bundle\Controller\SymfonyProfilerController::syncAllAction } php_translation_profiler_translation_create_assets: path: /{token}/translation/create_assets methods: ["POST"] - defaults: { _controller: TranslationBundle:SymfonyProfiler:createAssets } + defaults: { _controller: Translation\Bundle\Controller\SymfonyProfilerController::createAssetsAction } diff --git a/Resources/config/routing_webui.yml b/Resources/config/routing_webui.yml index 9aebda46..780f5f96 100644 --- a/Resources/config/routing_webui.yml +++ b/Resources/config/routing_webui.yml @@ -2,24 +2,24 @@ translation_index: path: /_trans/{configName} methods: [GET] - defaults: { _controller: TranslationBundle:WebUI:index, configName: null } + defaults: { _controller: Translation\Bundle\Controller\WebUIController::indexAction, configName: null } translation_show: path: /_trans/{configName}/{locale}/{domain} methods: [GET] - defaults: { _controller: TranslationBundle:WebUI:show } + defaults: { _controller: Translation\Bundle\Controller\WebUIController::showAction } translation_create: path: /_trans/{configName}/{locale}/{domain}/new methods: [POST] - defaults: { _controller: TranslationBundle:WebUI:create } + defaults: { _controller: Translation\Bundle\Controller\WebUIController::createAction } translation_edit: path: /_trans/{configName}/{locale}/{domain} methods: [POST] - defaults: { _controller: TranslationBundle:WebUI:edit } + defaults: { _controller: Translation\Bundle\Controller\WebUIController::editAction } translation_delete: path: /_trans/{configName}/{locale}/{domain} methods: [DELETE] - defaults: { _controller: TranslationBundle:WebUI:delete } + defaults: { _controller: Translation\Bundle\Controller\WebUIController::deleteAction } From 689d3245ffc66f2dfa6e217b9476cc9096448460 Mon Sep 17 00:00:00 2001 From: Nico Stapelbroek Date: Tue, 26 Jun 2018 13:58:36 +0200 Subject: [PATCH 048/234] Fix the FileDumper::setBackup() method deprecation notice (#236) * Fix the FileDumper::setBackup() method deprecation notice Also adds a compiler pass to assure the service definition remains unchanged on older Symfony versions * Convert logic in compiler pass to fail-fast and fix styleci issues --- .../CompilerPass/FileDumperBackupPass.php | 34 +++++++++++++++++++ Resources/config/services.yml | 2 -- TranslationBundle.php | 2 ++ 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 DependencyInjection/CompilerPass/FileDumperBackupPass.php diff --git a/DependencyInjection/CompilerPass/FileDumperBackupPass.php b/DependencyInjection/CompilerPass/FileDumperBackupPass.php new file mode 100644 index 00000000..0b7b48ed --- /dev/null +++ b/DependencyInjection/CompilerPass/FileDumperBackupPass.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\DependencyInjection\CompilerPass; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Kernel; + +/** + * The FileDumper::setBackup is deprecated since Symfony 4.1. + * This compiler pass assures our service definition remains unchanged for older symfony versions (3 or lower) + * while keeping the latest version clean of deprecation notices. + */ +class FileDumperBackupPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (Kernel::MAJOR_VERSION >= 4) { + return; + } + + $definition = $container->getDefinition('php_translation.storage.xlf_dumper'); + $definition->addMethodCall('setBackup', [false]); + } +} diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 355ca6ec..5ef264e9 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -53,8 +53,6 @@ services: class: Translation\SymfonyStorage\Dumper\XliffDumper tags: - { name: translation.dumper, alias: xlf, legacy-alias: xliff } - calls: - - [setBackup, [false]] php_translation.catalogue_counter: public: true diff --git a/TranslationBundle.php b/TranslationBundle.php index f8976c04..2c4cfff8 100644 --- a/TranslationBundle.php +++ b/TranslationBundle.php @@ -16,6 +16,7 @@ use Translation\Bundle\DependencyInjection\CompilerPass\EditInPlacePass; use Translation\Bundle\DependencyInjection\CompilerPass\ExternalTranslatorPass; use Translation\Bundle\DependencyInjection\CompilerPass\ExtractorPass; +use Translation\Bundle\DependencyInjection\CompilerPass\FileDumperBackupPass; use Translation\Bundle\DependencyInjection\CompilerPass\LoaderOrReaderPass; use Translation\Bundle\DependencyInjection\CompilerPass\StoragePass; use Translation\Bundle\DependencyInjection\CompilerPass\SymfonyProfilerPass; @@ -35,5 +36,6 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new StoragePass()); $container->addCompilerPass(new EditInPlacePass()); $container->addCompilerPass(new LoaderOrReaderPass()); + $container->addCompilerPass(new FileDumperBackupPass()); } } From 9bf308cf54cd92dc859e969b2439f68e27773b09 Mon Sep 17 00:00:00 2001 From: Nico Stapelbroek Date: Tue, 26 Jun 2018 14:01:20 +0200 Subject: [PATCH 049/234] Set twig strict_variables config to suppress the deprecation notices (#237) This is a behavioural change: the default value is 'false' compared to the new default value of '%kernel.debug%' which eventually evaluates to true when running the tests. Since it seems that this configuration is only used in the functional tests, there should be nothing functionally breaking in the bundle --- Tests/Functional/app/config/framework.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/Functional/app/config/framework.yml b/Tests/Functional/app/config/framework.yml index 8822b255..a1ee7822 100644 --- a/Tests/Functional/app/config/framework.yml +++ b/Tests/Functional/app/config/framework.yml @@ -14,5 +14,6 @@ framework: engines: ['twig'] twig: + strict_variables: "%kernel.debug%" #supresses deprecation notices about the default value TwigBundle pre version 5 paths: "%test.root_dir%/Resources/views": App From 234a082e635881716bc0b1c4ac0ffef286f90fac Mon Sep 17 00:00:00 2001 From: Olivier Dolbeau Date: Tue, 26 Jun 2018 14:04:14 +0200 Subject: [PATCH 050/234] Ensure storage exists before using it in commands (#239) --- Command/DeleteObsoleteCommand.php | 9 +- Command/DownloadCommand.php | 9 +- Command/StorageTrait.php | 31 +++++++ Command/SyncCommand.php | 10 +-- Tests/Functional/Command/SyncCommandTest.php | 86 ++++++++++++++++++++ 5 files changed, 124 insertions(+), 21 deletions(-) create mode 100644 Command/StorageTrait.php create mode 100644 Tests/Functional/Command/SyncCommandTest.php diff --git a/Command/DeleteObsoleteCommand.php b/Command/DeleteObsoleteCommand.php index 911af231..4da26b80 100644 --- a/Command/DeleteObsoleteCommand.php +++ b/Command/DeleteObsoleteCommand.php @@ -28,15 +28,10 @@ */ class DeleteObsoleteCommand extends Command { - use BundleTrait; + use BundleTrait, StorageTrait; protected static $defaultName = 'translation:delete-obsolete'; - /** - * @var StorageManager - */ - private $storageManager; - /** * @var ConfigurationManager */ @@ -94,7 +89,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->configureBundleDirs($input, $config); $this->catalogueManager->load($this->catalogueFetcher->getCatalogues($config, $locales)); - $storage = $this->storageManager->getStorage($configName); + $storage = $this->getStorage($configName); $messages = $this->catalogueManager->findMessages(['locale' => $inputLocale, 'isObsolete' => true]); $messageCount = count($messages); diff --git a/Command/DownloadCommand.php b/Command/DownloadCommand.php index 1b201c8c..6d853776 100644 --- a/Command/DownloadCommand.php +++ b/Command/DownloadCommand.php @@ -27,15 +27,10 @@ */ class DownloadCommand extends Command { - use BundleTrait; + use BundleTrait, StorageTrait; protected static $defaultName = 'translation:download'; - /** - * @var StorageManager - */ - private $storageManager; - /** * @var ConfigurationManager */ @@ -76,7 +71,7 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { $configName = $input->getArgument('configuration'); - $storage = $this->storageManager->getStorage($configName); + $storage = $this->getStorage($configName); $configuration = $this->configurationManager->getConfiguration($configName); $this->configureBundleDirs($input, $configuration); diff --git a/Command/StorageTrait.php b/Command/StorageTrait.php new file mode 100644 index 00000000..40d7e395 --- /dev/null +++ b/Command/StorageTrait.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Command; + +use Translation\Bundle\Service\StorageManager; + +trait StorageTrait +{ + /** + * @var StorageManager + */ + private $storageManager; + + private function getStorage($configName) + { + if (null === $storage = $this->storageManager->getStorage($configName)) { + $availableStorages = $this->storageManager->getNames(); + + throw new \InvalidArgumentException(sprintf('Unknown storage "%s". Available storages are "%s".', $configName, implode('", "', $availableStorages))); + } + } +} diff --git a/Command/SyncCommand.php b/Command/SyncCommand.php index 461ba6f5..f6298ca6 100644 --- a/Command/SyncCommand.php +++ b/Command/SyncCommand.php @@ -23,12 +23,9 @@ */ class SyncCommand extends Command { - protected static $defaultName = 'translation:sync'; + use StorageTrait; - /** - * @var StorageManager - */ - private $storageManager; + protected static $defaultName = 'translation:sync'; /** * @param StorageManager $storageManager @@ -66,7 +63,6 @@ protected function execute(InputInterface $input, OutputInterface $output) return; } - $configName = $input->getArgument('configuration'); - $this->storageManager->getStorage($configName)->sync($direction); + $this->getStorage($input->getArgument('configuration'))->sync($direction); } } diff --git a/Tests/Functional/Command/SyncCommandTest.php b/Tests/Functional/Command/SyncCommandTest.php new file mode 100644 index 00000000..0df4dd76 --- /dev/null +++ b/Tests/Functional/Command/SyncCommandTest.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Tests\Functional\Command; + +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Tester\CommandTester; +use Translation\Bundle\Tests\Functional\BaseTestCase; + +class SyncCommandTest extends BaseTestCase +{ + protected function setUp() + { + parent::setUp(); + $this->kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yml'); + + file_put_contents(__DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' + + + + + + translated.heading + My translated heading + + + + + translated.paragraph0 + My translated paragraph0 + + + + + foobar.html.twig:9 + + + translated.paragraph1 + My translated paragraph1 + + + + + not.in.source + This is not in the source code + + + + + +XML + ); + } + + public function testExecute() + { + $this->bootKernel(); + $application = new Application($this->kernel); + + $container = $this->getContainer(); + $application->add($container->get('php_translator.console.sync')); + + $command = $application->find('translation:sync'); + $commandTester = new CommandTester($command); + + try { + $commandTester->execute([ + 'command' => $command->getName(), + 'configuration' => 'fail', + ]); + + $this->fail('The command should fail when called with an unknown configuration key.'); + } catch (\InvalidArgumentException $e) { + $this->assertRegExp('|Unknown storage "fail"\.|s', $e->getMessage()); + $this->assertRegExp('|Available storages are "app"\.|s', $e->getMessage()); + } + } +} From 401a33ec5686689bd32894122b5ed9ceb7465396 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 26 Jun 2018 14:30:55 +0200 Subject: [PATCH 051/234] Fixes from sensiolabs insight (#240) * Make files non executable * Bugfix, StorageTrait has to return the storage * cs --- Command/StorageTrait.php | 9 +++++++++ Resources/views/WebUI/base.html.twig | 0 Resources/views/WebUI/index.html.twig | 0 Resources/views/WebUI/show.html.twig | 0 4 files changed, 9 insertions(+) mode change 100755 => 100644 Resources/views/WebUI/base.html.twig mode change 100755 => 100644 Resources/views/WebUI/index.html.twig mode change 100755 => 100644 Resources/views/WebUI/show.html.twig diff --git a/Command/StorageTrait.php b/Command/StorageTrait.php index 40d7e395..06cd359a 100644 --- a/Command/StorageTrait.php +++ b/Command/StorageTrait.php @@ -20,6 +20,13 @@ trait StorageTrait */ private $storageManager; + /** + * @param string $configName + * + * @return \Translation\Bundle\Service\StorageService + * + * @throws \InvalidArgumentException + */ private function getStorage($configName) { if (null === $storage = $this->storageManager->getStorage($configName)) { @@ -27,5 +34,7 @@ private function getStorage($configName) throw new \InvalidArgumentException(sprintf('Unknown storage "%s". Available storages are "%s".', $configName, implode('", "', $availableStorages))); } + + return $storage; } } diff --git a/Resources/views/WebUI/base.html.twig b/Resources/views/WebUI/base.html.twig old mode 100755 new mode 100644 diff --git a/Resources/views/WebUI/index.html.twig b/Resources/views/WebUI/index.html.twig old mode 100755 new mode 100644 diff --git a/Resources/views/WebUI/show.html.twig b/Resources/views/WebUI/show.html.twig old mode 100755 new mode 100644 From 130d2972f03532a304f2baa8b5bb360d50cb2b12 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 26 Jun 2018 14:53:29 +0200 Subject: [PATCH 052/234] Support stable version for php-translation packages (#241) --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index f8cb3ad9..066f0b26 100644 --- a/composer.json +++ b/composer.json @@ -18,14 +18,14 @@ "symfony/finder": "^2.7 || ^3.0 || ^4.0", "symfony/intl": "^2.7 || ^3.0 || ^4.0", - "php-translation/common": "^0.3", - "php-translation/symfony-storage": "^0.5.0", + "php-translation/common": "^1.0", + "php-translation/symfony-storage": "^1.0", "php-translation/extractor": "^1.3", "nyholm/nsa": "^1.1" }, "require-dev": { "symfony/phpunit-bridge": "^3.4 || ^4.0", - "php-translation/translator": "^0.1", + "php-translation/translator": "^1.0", "php-http/curl-client": "^1.7", "php-http/message": "^1.6", "php-http/message-factory": "^1.0.2", From c9c04e1f3aa2b851c11aa0e1fbfdc866287aa6bb Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 26 Jun 2018 14:53:51 +0200 Subject: [PATCH 053/234] Only measure code coverage on Unit tests. (#242) --- .travis.yml | 1 - composer.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 27b527c1..f8df3bc7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,7 +54,6 @@ matrix: - env: STABILITY="dev" before_install: - - if [[ $COVERAGE == true ]]; then echo "max_execution_time = 600" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; fi - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi - if ! [ -z "$STABILITY" ]; then composer config minimum-stability ${STABILITY}; fi; - if ! [ -z "$DEPENDENCIES" ]; then composer require --no-update ${DEPENDENCIES}; fi; diff --git a/composer.json b/composer.json index 066f0b26..ef11a207 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,7 @@ }, "scripts": { "test": "vendor/bin/simple-phpunit", - "test-ci": "vendor/bin/simple-phpunit --coverage-text --coverage-clover=build/coverage.xml" + "test-ci": "vendor/bin/simple-phpunit --coverage-text --coverage-clover=build/coverage.xml Tests/Unit" }, "extra": { "branch-alias": { From 359b8cdbd8c5d40ac11d322f59b5f5c5f4c7c2b6 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 3 Jul 2018 11:33:01 +0200 Subject: [PATCH 054/234] Only add value from `@desc` if catalogue has the default locale (#243) --- Resources/config/services.yml | 2 +- Service/Importer.php | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 5ef264e9..6d374777 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -32,7 +32,7 @@ services: php_translation.importer: public: true class: Translation\Bundle\Service\Importer - arguments: ["@php_translation.extractor", "@twig"] + arguments: ["@php_translation.extractor", "@twig", "%php_translation.default_locale%"] php_translation.cache_clearer: public: true diff --git a/Service/Importer.php b/Service/Importer.php index ad0287a0..1559dadc 100644 --- a/Service/Importer.php +++ b/Service/Importer.php @@ -44,14 +44,21 @@ final class Importer */ private $twig; + /** + * @var string + */ + private $defaultLocale; + /** * @param Extractor $extractor * @param \Twig_Environment $twig + * @param string $defaultLocale */ - public function __construct(Extractor $extractor, \Twig_Environment $twig) + public function __construct(Extractor $extractor, \Twig_Environment $twig, $defaultLocale) { $this->extractor = $extractor; $this->twig = $twig; + $this->defaultLocale = $defaultLocale; } /** @@ -103,7 +110,7 @@ public function extractToCatalogues(Finder $finder, array $catalogues, array $co $result->set($key, $newTranslation, $domain); // We do not want "translation" key stored anywhere. $meta->removeAllInCategory('translation'); - } elseif (null !== $newTranslation = $meta->getDesc()) { + } elseif (null !== $newTranslation = $meta->getDesc() && $catalogue->getLocale() === $this->defaultLocale) { $result->set($key, $newTranslation, $domain); } } From 380d979a2e17d4051c5347587745a148c667d3ee Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Wed, 4 Jul 2018 16:44:53 +0200 Subject: [PATCH 055/234] Prepare for 0.8.0 (#246) * Prepare for 0.8.0 * minor * Add missing parentheses to method name * Added note --- Changelog.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Changelog.md b/Changelog.md index 8fb06fb9..d19ac6a8 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,26 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.8.0 + +### Added + +- Bootstrap 4.1 CSS for web UI +- Support for stable `php-translation` dependencies + +### Fixed + +- Only add translation form `@desc` annotation to the default locale +- Ensure storage exists before using it in commands +- `FileDumper::setBackup()` deprecation notice +- Twig `strict_variabels` deprecation notice +- Avoid global bootstrap overrides - apply styles via new .configs CSS class + +### Changed + +- The `FallbackTranslator` will not try to translate to an empty locale. This could be considered as a BC break + since now it will return the translation key instead of whatever the translator service returned (usually the translated string in original language). + ## 0.7.0 ### Added From 037b945f5acb35090f916464ff77dbb2ebc765d8 Mon Sep 17 00:00:00 2001 From: Dylan Ballandras Date: Thu, 2 Aug 2018 10:05:02 +0200 Subject: [PATCH 056/234] Replace unicode close icon by the Bootstrap Icon (#248) It wasn't showing on all devices. I replaced it by the one of the [Boostrap documentation](https://getbootstrap.com/docs/4.1/utilities/close-icon/) --- Resources/views/WebUI/show.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/views/WebUI/show.html.twig b/Resources/views/WebUI/show.html.twig index e4fddb2a..f553d199 100644 --- a/Resources/views/WebUI/show.html.twig +++ b/Resources/views/WebUI/show.html.twig @@ -78,7 +78,7 @@ {% endif %} {{ message.key }} {% if allow_delete %} - + × {% endif %} - - + + + From 135f1730300fb49df102ab04995fdb1f626b3fbc Mon Sep 17 00:00:00 2001 From: Olivier Dolbeau Date: Wed, 15 Jan 2020 10:51:19 +0100 Subject: [PATCH 154/234] Always send X-Requested-With header when making an AJAX call --- Resources/public/js/editInPlace.js | 2 ++ Resources/public/js/webui.js | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Resources/public/js/editInPlace.js b/Resources/public/js/editInPlace.js index dc16e48c..aa412d9f 100644 --- a/Resources/public/js/editInPlace.js +++ b/Resources/public/js/editInPlace.js @@ -108,6 +108,8 @@ var TranslationBundleEditInPlace = function(saveUrl) { }; httpRequest.open('POST', saveUrl, true); + httpRequest.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + httpRequest.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); httpRequest.send(JSON.stringify(regions)); } diff --git a/Resources/public/js/webui.js b/Resources/public/js/webui.js index 5570f600..7e847fea 100755 --- a/Resources/public/js/webui.js +++ b/Resources/public/js/webui.js @@ -28,6 +28,7 @@ function editTranslation(el) { xmlhttp.open("POST", editUrl, true); xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + xmlhttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xmlhttp.send(JSON.stringify({message: el.value, key: el.getAttribute("data-key")})); } @@ -68,6 +69,7 @@ function createTranslation(el, url) { xmlhttp.open("POST", url, true); xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + xmlhttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xmlhttp.send(JSON.stringify({message: messageInput.value, key: keyInput.value})); return false; @@ -105,6 +107,7 @@ function deleteTranslation(el) { xmlhttp.open("DELETE", editUrl, true); xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + xmlhttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xmlhttp.send(JSON.stringify({key: messageKey})); } @@ -156,4 +159,4 @@ function showAllMessages(el) { var element = elements[i]; element.classList.remove("d-none"); } -} \ No newline at end of file +} From 8ad8ea95ef86f25a3f0def1275637e70a1f13b2b Mon Sep 17 00:00:00 2001 From: Mathieu Santostefano Date: Fri, 17 Jan 2020 11:24:34 +0100 Subject: [PATCH 155/234] Update changelog --- Changelog.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Changelog.md b/Changelog.md index c5524294..df645082 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,16 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.11.2 + +### Added + +- Support Symfony Profiler dark mode + +### Fixed + +- Add missing AJAX requests headers ('X-Requested-With') + ## 0.11.1 ### Fixed From 0f9cddc4139bb4303017bf5dccbe253bfae86656 Mon Sep 17 00:00:00 2001 From: Axel Guckelsberger Date: Sat, 18 Jan 2020 11:33:50 +0100 Subject: [PATCH 156/234] catch exceptions when storing translations (#391) --- Controller/WebUIController.php | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index 80520514..c6f6c268 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -76,7 +76,7 @@ public function __construct( public function indexAction(?string $configName = null): Response { if (!$this->isWebUIEnabled) { - return new Response('You are not allowed here. Check you config. ', 400); + return new Response('You are not allowed here. Check your config.', Response::HTTP_BAD_REQUEST); } $config = $this->configurationManager->getConfiguration($configName); @@ -124,7 +124,7 @@ public function indexAction(?string $configName = null): Response public function showAction(string $configName, string $locale, string $domain): Response { if (!$this->isWebUIEnabled) { - return new Response('You are not allowed here. Check you config. ', 400); + return new Response('You are not allowed here. Check your config.', Response::HTTP_BAD_REQUEST); } $config = $this->configurationManager->getConfiguration($configName); @@ -154,7 +154,7 @@ public function showAction(string $configName, string $locale, string $domain): public function createAction(Request $request, string $configName, string $locale, string $domain): Response { if (!$this->isWebUIEnabled || !$this->isWebUIAllowCreate) { - return new Response('You are not allowed to create. Check you config. ', 400); + return new Response('You are not allowed to create. Check your config.', Response::HTTP_BAD_REQUEST); } /** @var StorageService $storage */ @@ -166,13 +166,15 @@ public function createAction(Request $request, string $configName, string $local $message = $message->withLocale($locale); $this->validateMessage($message, ['Create']); } catch (MessageValidationException $e) { - return new Response($e->getMessage(), 400); + return new Response($e->getMessage(), Response::HTTP_BAD_REQUEST); } try { $storage->create($message); } catch (StorageException $e) { throw new BadRequestHttpException(\sprintf('Key "%s" does already exist for "%s" on domain "%s".', $message->getKey(), $locale, $domain), $e); + } catch (\Exception $e) { + return new Response($e->getMessage(), Response::HTTP_BAD_REQUEST); } return $this->render('@Translation/WebUI/create.html.twig', [ @@ -183,7 +185,7 @@ public function createAction(Request $request, string $configName, string $local public function editAction(Request $request, string $configName, string $locale, string $domain): Response { if (!$this->isWebUIEnabled) { - return new Response('You are not allowed here. Check you config. ', 400); + return new Response('You are not allowed here. Check your config.', Response::HTTP_BAD_REQUEST); } try { @@ -192,12 +194,16 @@ public function editAction(Request $request, string $configName, string $locale, $message = $message->withLocale($locale); $this->validateMessage($message, ['Edit']); } catch (MessageValidationException $e) { - return new Response($e->getMessage(), 400); + return new Response($e->getMessage(), Response::HTTP_BAD_REQUEST); } /** @var StorageService $storage */ $storage = $this->storageManager->getStorage($configName); - $storage->update($message); + try { + $storage->update($message); + } catch (\Exception $e) { + return new Response($e->getMessage(), Response::HTTP_BAD_REQUEST); + } return new Response('Translation updated'); } @@ -205,7 +211,7 @@ public function editAction(Request $request, string $configName, string $locale, public function deleteAction(Request $request, string $configName, string $locale, string $domain): Response { if (!$this->isWebUIEnabled || !$this->isWebUIAllowDelete) { - return new Response('You are not allowed to create. Check you config. ', 400); + return new Response('You are not allowed to create. Check your config.', Response::HTTP_BAD_REQUEST); } try { @@ -214,12 +220,16 @@ public function deleteAction(Request $request, string $configName, string $local $message = $message->withDomain($domain); $this->validateMessage($message, ['Delete']); } catch (MessageValidationException $e) { - return new Response($e->getMessage(), 400); + return new Response($e->getMessage(), Response::HTTP_BAD_REQUEST); } /** @var StorageService $storage */ $storage = $this->storageManager->getStorage($configName); - $storage->delete($locale, $domain, $message->getKey()); + try { + $storage->delete($locale, $domain, $message->getKey()); + } catch (\Exception $e) { + return new Response($e->getMessage(), Response::HTTP_BAD_REQUEST); + } return new Response('Message was deleted'); } From 6a0e645cf643ccc1b558a8eb865cd84c67e7e5dd Mon Sep 17 00:00:00 2001 From: axi Date: Sat, 18 Jan 2020 12:46:30 +0100 Subject: [PATCH 157/234] Add command to delete empty messages (#349) * Add command to delete empty messages * Updated baseline Co-authored-by: Tobias Nyholm --- Catalogue/CatalogueManager.php | 6 +- Command/DeleteEmptyCommand.php | 131 +++++++++++++++++++++++++++++++++ phpstan-baseline.neon | 5 ++ 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 Command/DeleteEmptyCommand.php diff --git a/Catalogue/CatalogueManager.php b/Catalogue/CatalogueManager.php index eb83d500..cd8089d7 100644 --- a/Catalogue/CatalogueManager.php +++ b/Catalogue/CatalogueManager.php @@ -82,6 +82,7 @@ public function findMessages(array $config = []): array $isNew = $config['isNew'] ?? null; $isObsolete = $config['isObsolete'] ?? null; $isApproved = $config['isApproved'] ?? null; + $isEmpty = $config['isEmpty'] ?? null; $messages = []; $catalogues = []; @@ -106,7 +107,7 @@ public function findMessages(array $config = []): array } } - $messages = \array_filter($messages, static function (CatalogueMessage $m) use ($isNew, $isObsolete, $isApproved) { + $messages = \array_filter($messages, static function (CatalogueMessage $m) use ($isNew, $isObsolete, $isApproved, $isEmpty) { if (null !== $isNew && $m->isNew() !== $isNew) { return false; } @@ -116,6 +117,9 @@ public function findMessages(array $config = []): array if (null !== $isApproved && $m->isApproved() !== $isApproved) { return false; } + if (null !== $isEmpty && empty($m->getMessage()) !== $isEmpty) { + return false; + } return true; }); diff --git a/Command/DeleteEmptyCommand.php b/Command/DeleteEmptyCommand.php new file mode 100644 index 00000000..67c4e97a --- /dev/null +++ b/Command/DeleteEmptyCommand.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputArgument; +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 Translation\Bundle\Catalogue\CatalogueFetcher; +use Translation\Bundle\Catalogue\CatalogueManager; +use Translation\Bundle\Service\ConfigurationManager; +use Translation\Bundle\Service\StorageManager; + +/** + * @author Tobias Nyholm + */ +class DeleteEmptyCommand extends Command +{ + use BundleTrait; + use StorageTrait; + + protected static $defaultName = 'translation:delete-empty'; + + /** + * @var ConfigurationManager + */ + private $configurationManager; + + /** + * @var CatalogueManager + */ + private $catalogueManager; + + /** + * @var CatalogueFetcher + */ + private $catalogueFetcher; + + public function __construct( + StorageManager $storageManager, + ConfigurationManager $configurationManager, + CatalogueManager $catalogueManager, + CatalogueFetcher $catalogueFetcher + ) { + $this->storageManager = $storageManager; + $this->configurationManager = $configurationManager; + $this->catalogueManager = $catalogueManager; + $this->catalogueFetcher = $catalogueFetcher; + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setName(self::$defaultName) + ->setDescription('Delete all translations currently empty.') + ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') + ->addArgument('locale', InputArgument::OPTIONAL, 'The locale to use. If omitted, we use all configured locales.', null) + ->addOption('bundle', 'b', InputOption::VALUE_REQUIRED, 'The bundle you want remove translations from.') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $configName = $input->getArgument('configuration'); + $locales = []; + if (null !== $inputLocale = $input->getArgument('locale')) { + $locales = [$inputLocale]; + } + + $config = $this->configurationManager->getConfiguration($configName); + $this->configureBundleDirs($input, $config); + $this->catalogueManager->load($this->catalogueFetcher->getCatalogues($config, $locales)); + + $storage = $this->getStorage($configName); + $messages = $this->catalogueManager->findMessages(['locale' => $inputLocale, 'isEmpty' => true]); + + $messageCount = \count($messages); + if (0 === $messageCount) { + $output->writeln('No messages are empty'); + + return 0; + } + + if ($input->isInteractive()) { + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion(\sprintf('You are about to remove %d translations. Do you wish to continue? (y/N) ', $messageCount), false); + if (!$helper->ask($input, $output, $question)) { + return 0; + } + } + + $progress = null; + if (OutputInterface::VERBOSITY_NORMAL === $output->getVerbosity() && OutputInterface::VERBOSITY_QUIET !== $output->getVerbosity()) { + $progress = new ProgressBar($output, $messageCount); + } + foreach ($messages as $message) { + $storage->delete($message->getLocale(), $message->getDomain(), $message->getKey()); + if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) { + $output->writeln(\sprintf( + 'Deleted empty message "%s" from domain "%s" and locale "%s"', + $message->getKey(), + $message->getDomain(), + $message->getLocale() + )); + } + + if ($progress) { + $progress->advance(); + } + } + + if ($progress) { + $progress->finish(); + } + + return 0; + } +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e0a36220..e26ad678 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,5 +1,10 @@ parameters: ignoreErrors: + - + message: "#^Call to an undefined method Symfony\\\\Component\\\\Console\\\\Application\\:\\:getKernel\\(\\)\\.$#" + count: 1 + path: Command/DeleteEmptyCommand.php + - message: "#^Call to an undefined method Symfony\\\\Component\\\\Console\\\\Application\\:\\:getKernel\\(\\)\\.$#" count: 1 From 33e70842d4bb029896fc196517fb952df0120fa5 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sat, 18 Jan 2020 12:46:49 +0100 Subject: [PATCH 158/234] Add support for arbitrary options to StorageInterface::export()/import() (#378) * Add support for arbitrary options to StorageInterface::export()/import() * Added the same support for DownloadCommand --- Command/DownloadCommand.php | 17 ++++++++++++++++- Command/SyncCommand.php | 24 ++++++++++++++++++++++-- Service/StorageService.php | 22 +++++++++++----------- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/Command/DownloadCommand.php b/Command/DownloadCommand.php index 990093c6..c4532c0b 100644 --- a/Command/DownloadCommand.php +++ b/Command/DownloadCommand.php @@ -63,6 +63,7 @@ protected function configure(): void ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') ->addOption('cache', null, InputOption::VALUE_NONE, '[DEPRECATED] Cache is now automatically cleared when translations have changed.') ->addOption('bundle', 'b', InputOption::VALUE_REQUIRED, 'The bundle you want update translations from.') + ->addOption('export-config', 'exconf', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Options to send to the StorageInterface::export() function. Ie, when downloading. Example: --export-config foo:bar', []) ; } @@ -86,7 +87,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $translationsDirectory = $config->getOutputDir(); $md5BeforeDownload = $this->hashDirectory($translationsDirectory); - $catalogues = $storage->download(); + $exportOptions = $this->cleanParameters($input->getOption('export-config')); + $catalogues = $storage->download($exportOptions); $this->catalogueWriter->writeCatalogues($config, $catalogues); $translationsCount = 0; @@ -129,4 +131,17 @@ private function hashDirectory(string $directory) return \hash_final($hash); } + + public function cleanParameters(array $raw) + { + $config = []; + + foreach ($raw as $string) { + // Assert $string looks like "foo:bar" + list($key, $value) = \explode(':', $string, 2); + $config[$key][] = $value; + } + + return $config; + } } diff --git a/Command/SyncCommand.php b/Command/SyncCommand.php index b0cbc931..345affb3 100644 --- a/Command/SyncCommand.php +++ b/Command/SyncCommand.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Translation\Bundle\Service\StorageManager; use Translation\Bundle\Service\StorageService; @@ -40,7 +41,10 @@ protected function configure(): void ->setName(self::$defaultName) ->setDescription('Sync the translations with the remote storage') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') - ->addArgument('direction', InputArgument::OPTIONAL, 'Use "down" if local changes should be overwritten, otherwise "up"', 'down'); + ->addArgument('direction', InputArgument::OPTIONAL, 'Use "down" if local changes should be overwritten, otherwise "up"', 'down') + ->addOption('export-config', 'exconf', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Options to send to the StorageInterface::export() function. Ie, when downloading. Example: --export-config foo:bar', []) + ->addOption('import-config', 'imconf', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Options to send to the StorageInterface::import() function. Ie, when uploading. Example: --import-config foo:bar', []) + ; } protected function execute(InputInterface $input, OutputInterface $output): int @@ -60,8 +64,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - $this->getStorage($input->getArgument('configuration'))->sync($direction); + $export = $this->cleanParameters($input->getOption('export-config')); + $import = $this->cleanParameters($input->getOption('import-config')); + + $this->getStorage($input->getArgument('configuration'))->sync($direction, $import, $export); return 0; } + + public function cleanParameters(array $raw) + { + $config = []; + + foreach ($raw as $string) { + // Assert $string looks like "foo:bar" + list($key, $value) = \explode(':', $string, 2); + $config[$key][] = $value; + } + + return $config; + } } diff --git a/Service/StorageService.php b/Service/StorageService.php index b4ecb171..2442ae3c 100644 --- a/Service/StorageService.php +++ b/Service/StorageService.php @@ -55,14 +55,14 @@ public function __construct(CatalogueFetcher $catalogueFetcher, Configuration $c * * @return MessageCatalogue[] */ - public function download(): array + public function download(array $exportOptions = []): array { $catalogues = []; foreach ($this->config->getLocales() as $locale) { $catalogues[$locale] = new MessageCatalogue($locale); foreach ($this->remoteStorages as $storage) { if ($storage instanceof TransferableStorage) { - $storage->export($catalogues[$locale]); + $storage->export($catalogues[$locale], $exportOptions); } } } @@ -73,17 +73,17 @@ public function download(): array /** * Synchronize translations with remote. */ - public function sync(string $direction = self::DIRECTION_DOWN): void + public function sync(string $direction = self::DIRECTION_DOWN, array $importOptions = [], array $exportOptions = []): void { switch ($direction) { case self::DIRECTION_DOWN: - $this->mergeDown(); - $this->mergeUp(); + $this->mergeDown($exportOptions); + $this->mergeUp($importOptions); break; case self::DIRECTION_UP: - $this->mergeUp(); - $this->mergeDown(); + $this->mergeUp($importOptions); + $this->mergeDown($exportOptions); break; default: @@ -95,9 +95,9 @@ public function sync(string $direction = self::DIRECTION_DOWN): void * Download and merge all translations from remote storages down to your local storages. * Only the local storages will be changed. */ - public function mergeDown(): void + public function mergeDown(array $exportOptions = []): void { - $catalogues = $this->download(); + $catalogues = $this->download($exportOptions); foreach ($catalogues as $locale => $catalogue) { foreach ($catalogue->all() as $domain => $messages) { @@ -115,13 +115,13 @@ public function mergeDown(): void * * This will overwrite your remote copy. */ - public function mergeUp(): void + public function mergeUp(array $importOptions = []): void { $catalogues = $this->catalogueFetcher->getCatalogues($this->config); foreach ($catalogues as $catalogue) { foreach ($this->remoteStorages as $storage) { if ($storage instanceof TransferableStorage) { - $storage->import($catalogue); + $storage->import($catalogue, $importOptions); } } } From 6a7ad1a9a24381894d3465b4d2b740f685ca8550 Mon Sep 17 00:00:00 2001 From: Nyholm Date: Sat, 18 Jan 2020 12:50:06 +0100 Subject: [PATCH 159/234] Added changelog for 0.12.0 --- Changelog.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index df645082..8cb48d33 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,10 +2,17 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. -## 0.11.2 +## 0.12.0 ### Added +- Command to delete empty translations +- Ability to send arbitrary options to a TransferableStorage with the download and sync command. + +## 0.11.2 + +### Fixed + - Support Symfony Profiler dark mode ### Fixed From 8a820e3f5d04cc9c4abf9dc624e68c66361bf88c Mon Sep 17 00:00:00 2001 From: Nyholm Date: Sat, 18 Jan 2020 12:51:47 +0100 Subject: [PATCH 160/234] Changed version to 0.11.3 --- Changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index 8cb48d33..70436989 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,7 +2,7 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. -## 0.12.0 +## 0.11.3 ### Added @@ -11,7 +11,7 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" betwee ## 0.11.2 -### Fixed +### Added - Support Symfony Profiler dark mode From 2b096c1af13fea5dbf2ec176b8e046e191cee2ca Mon Sep 17 00:00:00 2001 From: Nic Wortel Date: Fri, 7 Feb 2020 15:09:16 +0100 Subject: [PATCH 161/234] Introduce a translation:check command, to check the current translation status --- Command/CheckCommand.php | 118 ++++++++++++++++++ Resources/config/console.yaml | 10 ++ Tests/Functional/Command/CheckCommandTest.php | 90 +++++++++++++ 3 files changed, 218 insertions(+) create mode 100644 Command/CheckCommand.php create mode 100644 Tests/Functional/Command/CheckCommandTest.php diff --git a/Command/CheckCommand.php b/Command/CheckCommand.php new file mode 100644 index 00000000..33a1630c --- /dev/null +++ b/Command/CheckCommand.php @@ -0,0 +1,118 @@ +configurationManager = $configurationManager; + $this->catalogueFetcher = $catalogueFetcher; + $this->importer = $importer; + $this->catalogueCounter = $catalogueCounter; + } + + protected function configure(): void + { + $this + ->setName(self::$defaultName) + ->setDescription('Check that all translations for a given locale are extracted.') + ->addArgument('locale', InputArgument::REQUIRED, 'The locale to check') + ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $config = $this->configurationManager->getConfiguration($input->getArgument('configuration')); + + $locale = $input->getArgument('locale'); + + $catalogues = $this->catalogueFetcher->getCatalogues($config, [$locale]); + $finder = $this->getConfiguredFinder($config); + + $result = $this->importer->extractToCatalogues( + $finder, + $catalogues, + [ + 'blacklist_domains' => $config->getBlacklistDomains(), + 'whitelist_domains' => $config->getWhitelistDomains(), + 'project_root' => $config->getProjectRoot(), + ] + ); + + $definedBefore = $this->catalogueCounter->getNumberOfDefinedMessages($catalogues[0]); + $definedAfter = $this->catalogueCounter->getNumberOfDefinedMessages($result->getMessageCatalogues()[0]); + + $newMessages = $definedAfter - $definedBefore; + + $io = new SymfonyStyle($input, $output); + + if ($newMessages > 0) { + $io->error(sprintf('%d new message(s) have been found, run bin/console translation:extract', $newMessages)); + + return 1; + } + + $io->success('No new translation messages'); + + return 0; + } + + private function getConfiguredFinder(Configuration $config): Finder + { + $finder = new Finder(); + $finder->in($config->getDirs()); + + foreach ($config->getExcludedDirs() as $exclude) { + $finder->notPath($exclude); + } + + foreach ($config->getExcludedNames() as $exclude) { + $finder->notName($exclude); + } + + return $finder; + } +} diff --git a/Resources/config/console.yaml b/Resources/config/console.yaml index f9eeb2a9..ed79c994 100644 --- a/Resources/config/console.yaml +++ b/Resources/config/console.yaml @@ -1,4 +1,14 @@ services: + Translation\Bundle\Command\CheckCommand: + public: true + arguments: + - '@Translation\Bundle\Service\ConfigurationManager' + - '@Translation\Bundle\Catalogue\CatalogueFetcher' + - '@Translation\Bundle\Service\Importer' + - '@Translation\Bundle\Catalogue\CatalogueCounter' + tags: + - { name: console.command, command: translation:check } + Translation\Bundle\Command\DeleteObsoleteCommand: public: true arguments: diff --git a/Tests/Functional/Command/CheckCommandTest.php b/Tests/Functional/Command/CheckCommandTest.php new file mode 100644 index 00000000..1af62b43 --- /dev/null +++ b/Tests/Functional/Command/CheckCommandTest.php @@ -0,0 +1,90 @@ +kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yaml'); + $this->bootKernel(); + $this->application = new Application($this->kernel); + + \file_put_contents(__DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' + + + + + + translated.heading + My translated heading + + + + + translated.paragraph0 + My translated paragraph0 + + + + + foobar.html.twig:9 + + + translated.paragraph1 + My translated paragraph1 + + + + + not.in.source + This is not in the source code + + + + +XML + ); + } + + public function testReportsMissingTranslations(): void + { + $commandTester = new CommandTester($this->application->find('translation:check')); + + $commandTester->execute(['locale' => 'sv', 'configuration' => 'app']); + + $this->assertStringContainsString( + '4 new message(s) have been found, run bin/console translation:extract', + $commandTester->getDisplay() + ); + $this->assertGreaterThan(0, $commandTester->getStatusCode()); + } + + public function testReportsNoNewTranslationMessages(): void + { + // run translation:extract first, so all translations are extracted + (new CommandTester($this->application->find('translation:extract')))->execute(['locale' => 'sv']); + + $commandTester = new CommandTester($this->application->find('translation:check')); + + $commandTester->execute(['locale' => 'sv', 'configuration' => 'app']); + + $this->assertStringContainsString( + 'No new translation messages', + $commandTester->getDisplay() + ); + $this->assertSame(0, $commandTester->getStatusCode()); + } +} From 360b332c8f28a4c87ed1e8b039b1151fa211fedd Mon Sep 17 00:00:00 2001 From: Nic Wortel Date: Fri, 7 Feb 2020 15:21:46 +0100 Subject: [PATCH 162/234] Also check for empty translation messages --- Command/CheckCommand.php | 34 +++++- Tests/Functional/Command/CheckCommandTest.php | 110 +++++++++++++++++- 2 files changed, 139 insertions(+), 5 deletions(-) diff --git a/Command/CheckCommand.php b/Command/CheckCommand.php index 33a1630c..c04b4de3 100644 --- a/Command/CheckCommand.php +++ b/Command/CheckCommand.php @@ -9,11 +9,14 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Finder\Finder; +use Symfony\Component\Translation\MessageCatalogueInterface; use Translation\Bundle\Catalogue\CatalogueCounter; use Translation\Bundle\Catalogue\CatalogueFetcher; use Translation\Bundle\Model\Configuration; use Translation\Bundle\Service\ConfigurationManager; use Translation\Bundle\Service\Importer; +use function array_filter; +use function count; final class CheckCommand extends Command { @@ -59,8 +62,7 @@ protected function configure(): void ->setName(self::$defaultName) ->setDescription('Check that all translations for a given locale are extracted.') ->addArgument('locale', InputArgument::REQUIRED, 'The locale to check') - ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') - ; + ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default'); } protected function execute(InputInterface $input, OutputInterface $output): int @@ -95,6 +97,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } + $emptyTranslations = $this->countEmptyTranslations($result->getMessageCatalogues()[0]); + + if ($emptyTranslations > 0) { + $io->error( + sprintf('%d messages have empty translations, please provide translations for them', $emptyTranslations) + ); + + return 1; + } + $io->success('No new translation messages'); return 0; @@ -115,4 +127,22 @@ private function getConfiguredFinder(Configuration $config): Finder return $finder; } + + private function countEmptyTranslations(MessageCatalogueInterface $catalogue): int + { + $total = 0; + + foreach ($catalogue->getDomains() as $domain) { + $emptyTranslations = array_filter( + $catalogue->all($domain), + function (string $message): bool { + return $message === ''; + } + ); + + $total += count($emptyTranslations); + } + + return $total; + } } diff --git a/Tests/Functional/Command/CheckCommandTest.php b/Tests/Functional/Command/CheckCommandTest.php index 1af62b43..c5074baf 100644 --- a/Tests/Functional/Command/CheckCommandTest.php +++ b/Tests/Functional/Command/CheckCommandTest.php @@ -6,6 +6,7 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; use Translation\Bundle\Tests\Functional\BaseTestCase; +use function file_put_contents; class CheckCommandTest extends BaseTestCase { @@ -18,11 +19,13 @@ protected function setUp(): void { parent::setUp(); - $this->kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yaml'); + $this->kernel->addConfigFile(__DIR__ . '/../app/config/normal_config.yaml'); $this->bootKernel(); $this->application = new Application($this->kernel); - \file_put_contents(__DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' + file_put_contents( + __DIR__ . '/../app/Resources/translations/messages.sv.xlf', + <<<'XML' @@ -72,7 +75,7 @@ public function testReportsMissingTranslations(): void $this->assertGreaterThan(0, $commandTester->getStatusCode()); } - public function testReportsNoNewTranslationMessages(): void + public function testReportsEmptyTranslationMessages(): void { // run translation:extract first, so all translations are extracted (new CommandTester($this->application->find('translation:extract')))->execute(['locale' => 'sv']); @@ -81,6 +84,107 @@ public function testReportsNoNewTranslationMessages(): void $commandTester->execute(['locale' => 'sv', 'configuration' => 'app']); + $this->assertStringContainsString( + '4 messages have empty translations, please provide translations for them', + $commandTester->getDisplay() + ); + $this->assertGreaterThan(0, $commandTester->getStatusCode()); + } + + public function testReportsNoNewTranslationMessages(): void + { + file_put_contents( + __DIR__ . '/../app/Resources/translations/messages.sv.xlf', + <<<'XML' + + + + + + Resources/views/translated.html.twig:5 + new + + + translated.title + My translated title + + + + + Resources/views/translated.html.twig:8 + + + translated.heading + My translated heading + + + + + Resources/views/translated.html.twig:9 + + + translated.paragraph0 + My translated paragraph0 + + + + + Resources/views/translated.html.twig:9 + + + translated.paragraph1 + My translated paragraph1 + + + + + Resources/views/translated.html.twig:11 + new + + + translated.paragraph2 + My translated paragraph2 + + + + + Resources/views/translated.html.twig:12 + Resources/views/translated.html.twig:12 + new + + + localized.email + My localized email + + + + + Resources/views/translated.html.twig:14 + new + + + translated.attribute + My translated attribute + + + + + obsolete + + + not.in.source + This is not in the source code + + + + +XML + ); + + $commandTester = new CommandTester($this->application->find('translation:check')); + + $commandTester->execute(['locale' => 'sv', 'configuration' => 'app']); + $this->assertStringContainsString( 'No new translation messages', $commandTester->getDisplay() From 59bc570a7b4b5bfe619306056de4cb4fbef8a5f6 Mon Sep 17 00:00:00 2001 From: Nic Wortel Date: Fri, 7 Feb 2020 15:28:56 +0100 Subject: [PATCH 163/234] Fix code style issues --- Command/CheckCommand.php | 13 ++++++------- Tests/Functional/Command/CheckCommandTest.php | 12 ++++++------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Command/CheckCommand.php b/Command/CheckCommand.php index c04b4de3..40c0c0d9 100644 --- a/Command/CheckCommand.php +++ b/Command/CheckCommand.php @@ -1,4 +1,5 @@ 0) { - $io->error(sprintf('%d new message(s) have been found, run bin/console translation:extract', $newMessages)); + $io->error(\sprintf('%d new message(s) have been found, run bin/console translation:extract', $newMessages)); return 1; } @@ -101,7 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($emptyTranslations > 0) { $io->error( - sprintf('%d messages have empty translations, please provide translations for them', $emptyTranslations) + \sprintf('%d messages have empty translations, please provide translations for them', $emptyTranslations) ); return 1; @@ -133,14 +132,14 @@ private function countEmptyTranslations(MessageCatalogueInterface $catalogue): i $total = 0; foreach ($catalogue->getDomains() as $domain) { - $emptyTranslations = array_filter( + $emptyTranslations = \array_filter( $catalogue->all($domain), function (string $message): bool { - return $message === ''; + return '' === $message; } ); - $total += count($emptyTranslations); + $total += \count($emptyTranslations); } return $total; diff --git a/Tests/Functional/Command/CheckCommandTest.php b/Tests/Functional/Command/CheckCommandTest.php index c5074baf..cd41f61b 100644 --- a/Tests/Functional/Command/CheckCommandTest.php +++ b/Tests/Functional/Command/CheckCommandTest.php @@ -1,4 +1,5 @@ kernel->addConfigFile(__DIR__ . '/../app/config/normal_config.yaml'); + $this->kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yaml'); $this->bootKernel(); $this->application = new Application($this->kernel); - file_put_contents( - __DIR__ . '/../app/Resources/translations/messages.sv.xlf', + \file_put_contents( + __DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' @@ -93,8 +93,8 @@ public function testReportsEmptyTranslationMessages(): void public function testReportsNoNewTranslationMessages(): void { - file_put_contents( - __DIR__ . '/../app/Resources/translations/messages.sv.xlf', + \file_put_contents( + __DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' From aed52a4c8824d229574c469ef5d97ca84c88a184 Mon Sep 17 00:00:00 2001 From: Nic Wortel Date: Fri, 7 Feb 2020 15:36:24 +0100 Subject: [PATCH 164/234] Search for a shorter string in the test This prevents issues in CI with a smaller console width. --- Tests/Functional/Command/CheckCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Functional/Command/CheckCommandTest.php b/Tests/Functional/Command/CheckCommandTest.php index cd41f61b..897b5a61 100644 --- a/Tests/Functional/Command/CheckCommandTest.php +++ b/Tests/Functional/Command/CheckCommandTest.php @@ -85,7 +85,7 @@ public function testReportsEmptyTranslationMessages(): void $commandTester->execute(['locale' => 'sv', 'configuration' => 'app']); $this->assertStringContainsString( - '4 messages have empty translations, please provide translations for them', + '4 messages have empty translations, please provide translations', $commandTester->getDisplay() ); $this->assertGreaterThan(0, $commandTester->getStatusCode()); From 50e54c56025ec57a9c1dc256e85637cbf6c11691 Mon Sep 17 00:00:00 2001 From: Axel Guckelsberger Date: Mon, 17 Feb 2020 00:55:16 -0800 Subject: [PATCH 165/234] add service definition for form field title extractor (#395) * added form field title extractor * trigger travis * added requested changes * use alias * use alias_id * add resources for visitor namespace * added binding * added missing definition --- DependencyInjection/TranslationExtension.php | 9 + Resources/config/extractors.yaml | 171 ++++++++----------- phpstan-baseline.neon | 5 - 3 files changed, 77 insertions(+), 108 deletions(-) diff --git a/DependencyInjection/TranslationExtension.php b/DependencyInjection/TranslationExtension.php index b7c1aab0..11ed972e 100644 --- a/DependencyInjection/TranslationExtension.php +++ b/DependencyInjection/TranslationExtension.php @@ -42,6 +42,8 @@ class TranslationExtension extends Extension */ public function load(array $configs, ContainerBuilder $container): void { + $container->setParameter('extractor_vendor_dir', $this->getExtractorVendorDirectory()); + $configuration = new Configuration($container); $config = $this->processConfiguration($configuration, $configs); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); @@ -232,4 +234,11 @@ public function getConfiguration(array $config, ContainerBuilder $container): Co { return new Configuration($container); } + + private function getExtractorVendorDirectory(): string + { + $vendorReflection = new \ReflectionClass(FormTypeChoices::class); + + return \dirname($vendorReflection->getFileName(), 4); + } } diff --git a/Resources/config/extractors.yaml b/Resources/config/extractors.yaml index 0f254dc3..f4247fe8 100644 --- a/Resources/config/extractors.yaml +++ b/Resources/config/extractors.yaml @@ -1,111 +1,76 @@ services: - Translation\Extractor\FileExtractor\PHPFileExtractor: - tags: - - { name: 'php_translation.extractor', type: 'php' } + _defaults: + bind: + $metadataFactory: '@validator' - Translation\Extractor\FileExtractor\TwigFileExtractor: - arguments: ["@twig"] - tags: - - { name: 'php_translation.extractor', type: 'twig' } + _instanceof: + PhpParser\NodeVisitor: + tags: + - { name: 'php_translation.visitor', type: 'php' } - # PHP Visitors: - Translation\Extractor\Visitor\Php\Symfony\ContainerAwareTrans: - tags: - - { name: 'php_translation.visitor', type: 'php' } + Translation\Extractor\FileExtractor\PHPFileExtractor: + tags: + - { name: 'php_translation.extractor', type: 'php' } - Translation\Extractor\Visitor\Php\Symfony\ContainerAwareTransChoice: - tags: - - { name: 'php_translation.visitor', type: 'php' } + Translation\Extractor\FileExtractor\TwigFileExtractor: + arguments: ['@twig'] + tags: + - { name: 'php_translation.extractor', type: 'twig' } - Translation\Extractor\Visitor\Php\Symfony\FlashMessage: - tags: - - { name: 'php_translation.visitor', type: 'php' } + # PHP Visitors: + Translation\Extractor\Visitor\Php\Symfony\: + resource: "%extractor_vendor_dir%/Visitor/Php/Symfony/*" - Translation\Extractor\Visitor\Php\Symfony\FormTypeChoices: - tags: - - { name: 'php_translation.visitor', type: 'php' } + Translation\Extractor\Visitor\Php\SourceLocationContainerVisitor: ~ - Translation\Extractor\Visitor\Php\Symfony\FormTypeEmptyValue: - tags: - - { name: 'php_translation.visitor', type: 'php' } + # Twig Visitors: + Translation\Extractor\Visitor\Twig\TwigVisitor: + tags: + - { name: 'php_translation.visitor', type: 'twig' } - Translation\Extractor\Visitor\Php\Symfony\FormTypeHelp: - tags: - - { name: 'php_translation.visitor', type: 'php' } - - Translation\Extractor\Visitor\Php\Symfony\FormTypeInvalidMessage: - tags: - - { name: 'php_translation.visitor', type: 'php' } - - Translation\Extractor\Visitor\Php\Symfony\FormTypeLabelExplicit: - tags: - - { name: 'php_translation.visitor', type: 'php' } - - Translation\Extractor\Visitor\Php\Symfony\FormTypeLabelImplicit: - tags: - - { name: 'php_translation.visitor', type: 'php' } - - Translation\Extractor\Visitor\Php\Symfony\FormTypePlaceholder: - tags: - - { name: 'php_translation.visitor', type: 'php' } - - Translation\Extractor\Visitor\Php\Symfony\ValidationAnnotation: - arguments: ['@validator'] - tags: - - { name: 'php_translation.visitor', type: 'php' } - - Translation\Extractor\Visitor\Php\SourceLocationContainerVisitor: - tags: - - { name: 'php_translation.visitor', type: 'php' } - - # Twig Visitors: - Translation\Extractor\Visitor\Twig\TwigVisitor: - tags: - - { name: 'php_translation.visitor', type: 'twig' } - - # To remove in next major release - php_translation.extractor.php: - parent: Translation\Extractor\FileExtractor\PHPFileExtractor - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.extractor.twig: - parent: Translation\Extractor\FileExtractor\TwigFileExtractor - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.extractor.php.visitor.ContainerAwareTrans: - parent: Translation\Extractor\Visitor\Php\Symfony\ContainerAwareTrans - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.extractor.php.visitor.ContainerAwareTransChoice: - parent: Translation\Extractor\Visitor\Php\Symfony\ContainerAwareTransChoice - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.extractor.php.visitor.FlashMessage: - parent: Translation\Extractor\Visitor\Php\Symfony\FlashMessage - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.extractor.php.visitor.FormTypeChoices: - parent: Translation\Extractor\Visitor\Php\Symfony\FormTypeChoices - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.extractor.php.visitor.FormTypeEmptyValue: - parent: Translation\Extractor\Visitor\Php\Symfony\FormTypeEmptyValue - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.extractor.php.visitor.FormTypeHelp: - parent: Translation\Extractor\Visitor\Php\Symfony\FormTypeHelp - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.extractor.php.visitor.FormTypeInvalidMessage: - parent: Translation\Extractor\Visitor\Php\Symfony\FormTypeInvalidMessage - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.extractor.php.visitor.FormTypeLabelExplicit: - parent: Translation\Extractor\Visitor\Php\Symfony\FormTypeLabelExplicit - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.extractor.php.visitor.FormTypeLabelImplicit: - parent: Translation\Extractor\Visitor\Php\Symfony\FormTypeLabelImplicit - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.extractor.php.visitor.FormTypePlaceholder: - parent: Translation\Extractor\Visitor\Php\Symfony\FormTypePlaceholder - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.extractor.php.visitor.ValidationAnnotation: - parent: Translation\Extractor\Visitor\Php\Symfony\ValidationAnnotation - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.extractor.php.visitor.SourceLocationContainerVisitor: - parent: Translation\Extractor\Visitor\Php\SourceLocationContainerVisitor - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.extractor.twig.factory.twig: - parent: Translation\Extractor\Visitor\Twig\TwigVisitor - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' + # To remove in next major release + php_translation.extractor.php: + alias: Translation\Extractor\FileExtractor\PHPFileExtractor + deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' + php_translation.extractor.twig: + alias: Translation\Extractor\FileExtractor\TwigFileExtractor + deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' + php_translation.extractor.php.visitor.ContainerAwareTrans: + alias: Translation\Extractor\Visitor\Php\Symfony\ContainerAwareTrans + deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' + php_translation.extractor.php.visitor.ContainerAwareTransChoice: + alias: Translation\Extractor\Visitor\Php\Symfony\ContainerAwareTransChoice + deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' + php_translation.extractor.php.visitor.FlashMessage: + alias: Translation\Extractor\Visitor\Php\Symfony\FlashMessage + deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' + php_translation.extractor.php.visitor.FormTypeChoices: + alias: Translation\Extractor\Visitor\Php\Symfony\FormTypeChoices + deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' + php_translation.extractor.php.visitor.FormTypeEmptyValue: + alias: Translation\Extractor\Visitor\Php\Symfony\FormTypeEmptyValue + deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' + php_translation.extractor.php.visitor.FormTypeHelp: + alias: Translation\Extractor\Visitor\Php\Symfony\FormTypeHelp + deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' + php_translation.extractor.php.visitor.FormTypeInvalidMessage: + alias: Translation\Extractor\Visitor\Php\Symfony\FormTypeInvalidMessage + deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' + php_translation.extractor.php.visitor.FormTypeLabelExplicit: + alias: Translation\Extractor\Visitor\Php\Symfony\FormTypeLabelExplicit + deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' + php_translation.extractor.php.visitor.FormTypeLabelImplicit: + alias: Translation\Extractor\Visitor\Php\Symfony\FormTypeLabelImplicit + deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' + php_translation.extractor.php.visitor.FormTypePlaceholder: + alias: Translation\Extractor\Visitor\Php\Symfony\FormTypePlaceholder + deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' + php_translation.extractor.php.visitor.ValidationAnnotation: + alias: Translation\Extractor\Visitor\Php\Symfony\ValidationAnnotation + deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' + php_translation.extractor.php.visitor.SourceLocationContainerVisitor: + alias: Translation\Extractor\Visitor\Php\SourceLocationContainerVisitor + deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' + php_translation.extractor.twig.factory.twig: + alias: Translation\Extractor\Visitor\Twig\TwigVisitor + deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e26ad678..be1f1d6d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -25,11 +25,6 @@ parameters: count: 1 path: Command/StatusCommand.php - - - message: "#^Call to method getValue\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\DataCollector\\\\Data\\.$#" - count: 1 - path: Controller/SymfonyProfilerController.php - - message: "#^Call to an undefined static method Symfony\\\\Component\\\\Intl\\\\Intl\\:\\:getLocaleBundle\\(\\)\\.$#" count: 1 From f6609f1f1b428612897575cc37a832db6b00b7d5 Mon Sep 17 00:00:00 2001 From: Nic Wortel Date: Tue, 18 Feb 2020 13:26:45 +0100 Subject: [PATCH 166/234] Rename translation:check to translation:check-missing --- Command/{CheckCommand.php => CheckMissingCommand.php} | 4 ++-- Resources/config/console.yaml | 4 ++-- .../{CheckCommandTest.php => CheckMissingCommandTest.php} | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) rename Command/{CheckCommand.php => CheckMissingCommand.php} (97%) rename Tests/Functional/Command/{CheckCommandTest.php => CheckMissingCommandTest.php} (97%) diff --git a/Command/CheckCommand.php b/Command/CheckMissingCommand.php similarity index 97% rename from Command/CheckCommand.php rename to Command/CheckMissingCommand.php index 40c0c0d9..e0162f19 100644 --- a/Command/CheckCommand.php +++ b/Command/CheckMissingCommand.php @@ -17,9 +17,9 @@ use Translation\Bundle\Service\ConfigurationManager; use Translation\Bundle\Service\Importer; -final class CheckCommand extends Command +final class CheckMissingCommand extends Command { - protected static $defaultName = 'translation:check'; + protected static $defaultName = 'translation:check-missing'; /** * @var ConfigurationManager diff --git a/Resources/config/console.yaml b/Resources/config/console.yaml index ed79c994..e2daeaf4 100644 --- a/Resources/config/console.yaml +++ b/Resources/config/console.yaml @@ -1,5 +1,5 @@ services: - Translation\Bundle\Command\CheckCommand: + Translation\Bundle\Command\CheckMissingCommand: public: true arguments: - '@Translation\Bundle\Service\ConfigurationManager' @@ -7,7 +7,7 @@ services: - '@Translation\Bundle\Service\Importer' - '@Translation\Bundle\Catalogue\CatalogueCounter' tags: - - { name: console.command, command: translation:check } + - { name: console.command, command: translation:check-missing } Translation\Bundle\Command\DeleteObsoleteCommand: public: true diff --git a/Tests/Functional/Command/CheckCommandTest.php b/Tests/Functional/Command/CheckMissingCommandTest.php similarity index 97% rename from Tests/Functional/Command/CheckCommandTest.php rename to Tests/Functional/Command/CheckMissingCommandTest.php index 897b5a61..f64dfbc9 100644 --- a/Tests/Functional/Command/CheckCommandTest.php +++ b/Tests/Functional/Command/CheckMissingCommandTest.php @@ -8,7 +8,7 @@ use Symfony\Component\Console\Tester\CommandTester; use Translation\Bundle\Tests\Functional\BaseTestCase; -class CheckCommandTest extends BaseTestCase +class CheckMissingCommandTest extends BaseTestCase { /** * @var Application @@ -64,7 +64,7 @@ protected function setUp(): void public function testReportsMissingTranslations(): void { - $commandTester = new CommandTester($this->application->find('translation:check')); + $commandTester = new CommandTester($this->application->find('translation:check-missing')); $commandTester->execute(['locale' => 'sv', 'configuration' => 'app']); @@ -80,7 +80,7 @@ public function testReportsEmptyTranslationMessages(): void // run translation:extract first, so all translations are extracted (new CommandTester($this->application->find('translation:extract')))->execute(['locale' => 'sv']); - $commandTester = new CommandTester($this->application->find('translation:check')); + $commandTester = new CommandTester($this->application->find('translation:check-missing')); $commandTester->execute(['locale' => 'sv', 'configuration' => 'app']); @@ -181,7 +181,7 @@ public function testReportsNoNewTranslationMessages(): void XML ); - $commandTester = new CommandTester($this->application->find('translation:check')); + $commandTester = new CommandTester($this->application->find('translation:check-missing')); $commandTester->execute(['locale' => 'sv', 'configuration' => 'app']); From fff5cca0197ee88bb3805b2ccca1a1e0f966fee4 Mon Sep 17 00:00:00 2001 From: Mathieu Santostefano Date: Fri, 21 Feb 2020 15:50:00 +0100 Subject: [PATCH 167/234] Prepare 0.12 release --- Changelog.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Changelog.md b/Changelog.md index 70436989..a06564af 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,13 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.12 + +### Added + +- Service definition for form field title extractor +- Command translation:check-missing + ## 0.11.3 ### Added From 2332658453034e47618e7e5f89df0a044d726ae9 Mon Sep 17 00:00:00 2001 From: Guite Date: Sun, 23 Feb 2020 13:13:59 +0100 Subject: [PATCH 168/234] allow null values in CheckMissingCommand --- Command/CheckMissingCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Command/CheckMissingCommand.php b/Command/CheckMissingCommand.php index e0162f19..9618e105 100644 --- a/Command/CheckMissingCommand.php +++ b/Command/CheckMissingCommand.php @@ -134,8 +134,8 @@ private function countEmptyTranslations(MessageCatalogueInterface $catalogue): i foreach ($catalogue->getDomains() as $domain) { $emptyTranslations = \array_filter( $catalogue->all($domain), - function (string $message): bool { - return '' === $message; + function (string $message = null): bool { + return null === $message || '' === $message; } ); From a5d1929df5af37a5f1f880bffdc7df5ae8df6637 Mon Sep 17 00:00:00 2001 From: Olivier Dolbeau Date: Tue, 25 Feb 2020 15:05:14 +0100 Subject: [PATCH 169/234] Prepare v0.12.1 --- Changelog.md | 6 ++++++ composer.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index a06564af..9e2609b6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,12 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.12.1 + +### Fixed + +- Allow null values in CheckMissingCommand + ## 0.12 ### Added diff --git a/composer.json b/composer.json index bafc819e..57309126 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "prefer-stable": true, "extra": { "branch-alias": { - "dev-master": "0.11-dev" + "dev-master": "0.12-dev" } } } From 09916edbfcc9b9688fb9a7a615fbba5d92196ba7 Mon Sep 17 00:00:00 2001 From: Olivier Dolbeau Date: Sat, 11 Apr 2020 13:12:19 +0200 Subject: [PATCH 170/234] Add missing command definition --- Resources/config/console.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Resources/config/console.yaml b/Resources/config/console.yaml index e2daeaf4..88b82e48 100644 --- a/Resources/config/console.yaml +++ b/Resources/config/console.yaml @@ -17,7 +17,17 @@ services: - '@Translation\Bundle\Catalogue\CatalogueManager' - '@Translation\Bundle\Catalogue\CatalogueFetcher' tags: - - { name: console.command, command: translation:delete-obsolete} + - { name: console.command, command: translation:delete-obsolete } + + Translation\Bundle\Command\DeleteEmptyCommand: + public: true + arguments: + - '@Translation\Bundle\Service\StorageManager' + - '@Translation\Bundle\Service\ConfigurationManager' + - '@Translation\Bundle\Catalogue\CatalogueManager' + - '@Translation\Bundle\Catalogue\CatalogueFetcher' + tags: + - { name: console.command, command: translation:delete-empty } Translation\Bundle\Command\DownloadCommand: public: true From 598a759af4a6ef5d5b7c45ab3604ae66f9d38a93 Mon Sep 17 00:00:00 2001 From: Pierre Grimaud Date: Mon, 27 Apr 2020 20:06:33 +0200 Subject: [PATCH 171/234] Fix typos --- Tests/Functional/app/config/framework.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Functional/app/config/framework.yaml b/Tests/Functional/app/config/framework.yaml index 3f53c7e7..2d4a4226 100644 --- a/Tests/Functional/app/config/framework.yaml +++ b/Tests/Functional/app/config/framework.yaml @@ -12,6 +12,6 @@ framework: type: 'yaml' twig: - strict_variables: "%kernel.debug%" #supresses deprecation notices about the default value TwigBundle pre version 5 + strict_variables: "%kernel.debug%" #suppresses deprecation notices about the default value TwigBundle pre version 5 paths: "%test.project_dir%/Resources/views": App From 8b901832a02bebea0c995f1c1ec64ec09d4018a8 Mon Sep 17 00:00:00 2001 From: Ewald Vanderveken Date: Wed, 16 Dec 2020 00:01:45 +0100 Subject: [PATCH 172/234] Fixed small typo --- Controller/WebUIController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index c6f6c268..d099104b 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -211,7 +211,7 @@ public function editAction(Request $request, string $configName, string $locale, public function deleteAction(Request $request, string $configName, string $locale, string $domain): Response { if (!$this->isWebUIEnabled || !$this->isWebUIAllowDelete) { - return new Response('You are not allowed to create. Check your config.', Response::HTTP_BAD_REQUEST); + return new Response('You are not allowed to delete. Check your config.', Response::HTTP_BAD_REQUEST); } try { From b38026e4dc572b19f8045102c11be63da68bf29b Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Fri, 26 Mar 2021 17:53:55 +0100 Subject: [PATCH 173/234] Adding github actions (#429) * Adding github actions * Remove phpunit/phpunit * Update baselines * Drop 7.1 * Updated meta files * minors * Apply PHP CS Fixer changes * Bugfix Co-authored-by: bocharsky-bw --- .gitattributes | 8 +- .github/workflows/ci.yml | 48 +++++++ .github/workflows/static.yml | 96 +++++++++++-- .scrutinizer.yml | 9 -- .travis.yml | 63 -------- Changelog.md | 6 + Command/DownloadCommand.php | 2 +- Controller/WebUIController.php | 2 +- Makefile | 31 ++-- Readme.md | 4 - .../ExternalTranslatorPassTest.php | 5 +- .../CompilerPass/ExtractorPassTest.php | 5 +- .../CompilerPass/StoragePassTest.php | 5 +- Twig/Visitor/DefaultApplyingNodeVisitor.php | 2 +- composer.json | 8 +- phpstan-baseline.neon | 10 -- psalm.baseline.xml | 136 ++++++++++++++++++ psalm.xml | 28 ++++ vendor-bin/php-cs-fixer/composer.json | 9 ++ vendor-bin/phpstan/composer.json | 10 ++ vendor-bin/psalm/composer.json | 9 ++ 21 files changed, 368 insertions(+), 128 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .scrutinizer.yml delete mode 100644 .travis.yml create mode 100644 psalm.baseline.xml create mode 100644 psalm.xml create mode 100644 vendor-bin/php-cs-fixer/composer.json create mode 100644 vendor-bin/phpstan/composer.json create mode 100644 vendor-bin/psalm/composer.json diff --git a/.gitattributes b/.gitattributes index a4f0f5b1..ef49f3fc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,8 +3,8 @@ .github export-ignore .gitignore export-ignore .php_cs export-ignore -.scrutinizer.yml export-ignore -.styleci.yml export-ignore -.travis.yml export-ignore -phpstan.neon.dist export-ignore +phpstan.neon.dist export-ignore +phpstan-baseline.neon export-ignore +psalm.baseline.xml export-ignore +psalm.xml export-ignore Tests export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..e6411bee --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + pull_request: + +jobs: + build: + name: Test + runs-on: Ubuntu-20.04 + strategy: + fail-fast: false + matrix: + php: [ '7.2', '7.3', '7.4', '8.0' ] + strategy: [ 'highest' ] + sf_version: [''] + include: + - php: 7.4 + strategy: 'lowest' + - php: 7.3 + sf_version: '3.*' + - php: 7.3 + sf_version: '4.*' + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: flex + + - name: Download dependencies + uses: ramsey/composer-install@v1 + env: + SYMFONY_REQUIRE: ${{ matrix.sf_version }} + with: + dependency-versions: ${{ matrix.strategy }} + composer-options: --no-interaction --prefer-dist --optimize-autoloader + + - name: Install PHPUnit + run: ./vendor/bin/simple-phpunit install + + - name: Run tests + run: ./vendor/bin/simple-phpunit + diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 2480e638..a02e633e 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -2,23 +2,99 @@ name: Static code analysis on: [pull_request] + jobs: phpstan: name: PHPStan - runs-on: ubuntu-latest + runs-on: Ubuntu-20.04 + steps: - - uses: actions/checkout@master - - name: Run PHPStan - uses: docker://jakzal/phpqa:php7.3-alpine + - name: Checkout code + uses: actions/checkout@v2 + + - name: Cache PHPStan + uses: actions/cache@v2 + with: + path: .github/.cache/phpstan/ + key: phpstan-${{ github.sha }} + restore-keys: phpstan- + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: none + + - name: Download dependencies + uses: ramsey/composer-install@v1 with: - args: phpstan analyze + composer-options: --no-interaction --prefer-dist --optimize-autoloader + + - name: Download PHPStan + run: composer bin phpstan update --no-interaction --no-progress + + - name: Execute PHPStan + run: vendor/bin/phpstan analyze --no-progress php-cs-fixer: name: PHP-CS-Fixer - runs-on: ubuntu-latest + runs-on: Ubuntu-20.04 + steps: - - uses: actions/checkout@master - - name: Run PHP-CS-Fixer - uses: docker://jakzal/phpqa:php7.3-alpine + - name: Checkout code + uses: actions/checkout@v2 + + - name: Cache PhpCsFixer + uses: actions/cache@v2 with: - args: php-cs-fixer fix --dry-run --diff-format udiff -vvv + path: .github/.cache/php-cs-fixer/ + key: php-cs-fixer-${{ github.sha }} + restore-keys: php-cs-fixer- + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: none + + - name: Download dependencies + uses: ramsey/composer-install@v1 + with: + composer-options: --no-interaction --prefer-dist --optimize-autoloader + + - name: Download PHP CS Fixer + run: composer bin php-cs-fixer update --no-interaction --no-progress + + - name: Execute PHP CS Fixer + run: vendor/bin/php-cs-fixer fix --diff-format udiff --dry-run + + psalm: + name: Psalm + runs-on: Ubuntu-20.04 + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Cache Psalm + uses: actions/cache@v2 + with: + path: .github/.cache/psalm/ + key: psalm-${{ github.sha }} + restore-keys: psalm- + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: none + + - name: Download dependencies + uses: ramsey/composer-install@v1 + with: + composer-options: --no-interaction --prefer-dist --optimize-autoloader + + - name: Download Psalm + run: composer bin psalm update --no-interaction --no-progress + + - name: Execute Psalm + run: vendor/bin/psalm --no-progress --output-format=github diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 28ef3ae4..00000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,9 +0,0 @@ -filter: - excluded_paths: [vendor/*, Tests/*] -checks: - php: - code_rating: true - duplication: true -tools: - external_code_coverage: - timeout: 1800 # Timeout in seconds. diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 128e366c..00000000 --- a/.travis.yml +++ /dev/null @@ -1,63 +0,0 @@ -language: php - -cache: - directories: - - $HOME/.composer/cache - -branches: - except: - - /^analysis-.*$/ - - /^patch-.*$/ - -env: - global: - - TEST_COMMAND="composer test" - - SYMFONY_PHPUNIT_VERSION="6.5" - - COMPOSER_MEMORY_LIMIT=-1 - -matrix: - fast_finish: true - include: - # Run test with code coverage - - php: 7.3 - env: COVERAGE=true TEST_COMMAND="composer test-ci" - - # Test with lowest dependencies - - php: 7.2 - env: COMPOSER_FLAGS="--prefer-lowest" SYMFONY_DEPRECATIONS_HELPER="weak" - - # Test the latest stable release - - php: 7.3 - - php: 7.4 - - # Force some major versions of Symfony - - php: 7.3 - env: SYMFONY_VERSION="3.*" - - php: 7.3 - env: SYMFONY_VERSION="4.*" - - # Latest commit to master - - php: 7.3 - env: STABILITY="dev" - - allow_failures: - # Dev-master is allowed to fail. - - env: STABILITY="dev" - -before_install: - - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi - - if ! [ -z "$STABILITY" ]; then composer config minimum-stability ${STABILITY}; fi; - - composer require --no-update symfony/flex - - if ! [ -z "$SYMFONY_VERSION" ]; then composer config extra.symfony.require "${SYMFONY_VERSION}"; fi; - -install: - - composer update ${COMPOSER_FLAGS} --prefer-dist --prefer-stable --no-interaction - -script: - - composer validate --strict --no-check-lock - - $TEST_COMMAND - -after_success: - - if [[ $COVERAGE = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi - - if [[ $COVERAGE = true ]]; then php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml; fi - diff --git a/Changelog.md b/Changelog.md index 9e2609b6..7f72c61b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,12 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## Unreleased + +### Removed + +- Support for PHP 7.1 + ## 0.12.1 ### Fixed diff --git a/Command/DownloadCommand.php b/Command/DownloadCommand.php index c4532c0b..a64999d4 100644 --- a/Command/DownloadCommand.php +++ b/Command/DownloadCommand.php @@ -75,7 +75,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $message = 'The --cache option is deprecated as it\'s now the default behaviour of this command.'; $io->note($message); - @\trigger_error($message, E_USER_DEPRECATED); + @\trigger_error($message, \E_USER_DEPRECATED); } $configName = $input->getArgument('configuration'); diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index d099104b..4ef7daa7 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -271,7 +271,7 @@ private function validateMessage(MessageInterface $message, array $validationGro { $errors = $this->validator->validate($message, null, $validationGroups); if (\count($errors) > 0) { - throw MessageValidationException::create(); + throw MessageValidationException::create(); } } } diff --git a/Makefile b/Makefile index db6bcf45..9b8cb4d5 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,33 @@ .PHONY: ${TARGETS} -DIR := ${CURDIR} -QA_IMAGE := jakzal/phpqa:php7.3-alpine +# https://www.gnu.org/software/make/manual/html_node/Force-Targets.html +always: -cs-fix: - @docker run --rm -v $(DIR):/project -w /project $(QA_IMAGE) php-cs-fixer fix --diff-format udiff -vvv +cs-fix: vendor + vendor/bin/php-cs-fixer fix --diff-format udiff -vvv -cs-diff: - @docker run --rm -v $(DIR):/project -w /project $(QA_IMAGE) php-cs-fixer fix --diff-format udiff --dry-run -vvv +cs-diff: vendor + vendor/bin/php-cs-fixer fix --diff-format udiff --dry-run -vvv -phpstan: - @docker run --rm -v $(DIR):/project -w /project $(QA_IMAGE) phpstan analyze +phpstan: vendor + vendor/bin/phpstan analyze -phpunit: +psalm: vendor + vendor/bin/psalm + +phpunit: vendor @vendor/bin/phpunit +.PHONY: baseline +baseline: vendor ## Generate baseline files + vendor/bin/phpstan analyze --generate-baseline + vendor/bin/psalm --set-baseline=psalm.baseline.xml + static: cs-diff phpstan test: static phpunit + +vendor: always + composer update --no-interaction + composer bin all install --no-interaction + vendor/bin/simple-phpunit install diff --git a/Readme.md b/Readme.md index cab8dbd8..5fae742e 100644 --- a/Readme.md +++ b/Readme.md @@ -1,13 +1,9 @@ # Translation Bundle [![Latest Version](https://img.shields.io/github/release/php-translation/symfony-bundle.svg?style=flat-square)](https://github.com/php-translation/symfony-bundle/releases) -[![Build Status](https://img.shields.io/travis/php-translation/symfony-bundle.svg?style=flat-square)](https://travis-ci.org/php-translation/symfony-bundle) -[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-translation/symfony-bundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-translation/symfony-bundle) -[![Quality Score](https://img.shields.io/scrutinizer/g/php-translation/symfony-bundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-translation/symfony-bundle) [![SensioLabsInsight](https://insight.sensiolabs.com/projects/c289ebe2-41c4-429f-afba-de2f905b9bdb/mini.png)](https://insight.sensiolabs.com/projects/c289ebe2-41c4-429f-afba-de2f905b9bdb) [![Total Downloads](https://img.shields.io/packagist/dt/php-translation/symfony-bundle.svg?style=flat-square)](https://packagist.org/packages/php-translation/symfony-bundle) - **Symfony integration for PHP Translation** ## Install diff --git a/Tests/Unit/DependencyInjection/CompilerPass/ExternalTranslatorPassTest.php b/Tests/Unit/DependencyInjection/CompilerPass/ExternalTranslatorPassTest.php index ac512577..4630de0c 100644 --- a/Tests/Unit/DependencyInjection/CompilerPass/ExternalTranslatorPassTest.php +++ b/Tests/Unit/DependencyInjection/CompilerPass/ExternalTranslatorPassTest.php @@ -24,10 +24,7 @@ protected function registerCompilerPass(ContainerBuilder $container): void $container->addCompilerPass(new ExternalTranslatorPass()); } - /** - * @test - */ - public function if_compiler_pass_collects_services_by_adding_method_calls_these_will_exist(): void + public function testIfCompilerPassCollectsServicesByAddingMethodCallsTheseWillExist(): void { $collectingService = new Definition(); $this->setDefinition('php_translation.translator_service.external_translator', $collectingService); diff --git a/Tests/Unit/DependencyInjection/CompilerPass/ExtractorPassTest.php b/Tests/Unit/DependencyInjection/CompilerPass/ExtractorPassTest.php index 6415147f..e8d19c7c 100644 --- a/Tests/Unit/DependencyInjection/CompilerPass/ExtractorPassTest.php +++ b/Tests/Unit/DependencyInjection/CompilerPass/ExtractorPassTest.php @@ -25,10 +25,7 @@ protected function registerCompilerPass(ContainerBuilder $container): void $container->addCompilerPass(new ExtractorPass()); } - /** - * @test - */ - public function if_compiler_pass_collects_services_by_adding_method_calls_these_will_exist(): void + public function testIfCompilerPassCollectsServicesByAddingMethodCallsTheseWillExist(): void { $collectingService = new Definition(); $this->setDefinition(Extractor::class, $collectingService); diff --git a/Tests/Unit/DependencyInjection/CompilerPass/StoragePassTest.php b/Tests/Unit/DependencyInjection/CompilerPass/StoragePassTest.php index b9e50ccc..0b1bd923 100644 --- a/Tests/Unit/DependencyInjection/CompilerPass/StoragePassTest.php +++ b/Tests/Unit/DependencyInjection/CompilerPass/StoragePassTest.php @@ -24,10 +24,7 @@ protected function registerCompilerPass(ContainerBuilder $container): void $container->addCompilerPass(new StoragePass()); } - /** - * @test - */ - public function if_compiler_pass_collects_services_by_adding_method_calls_these_will_exist(): void + public function testIfCompilerPassCollectsServicesByAddingMethodCallsTheseWillExist(): void { $collectingService = new Definition(); $this->setDefinition('php_translation.storage.foobar', $collectingService); diff --git a/Twig/Visitor/DefaultApplyingNodeVisitor.php b/Twig/Visitor/DefaultApplyingNodeVisitor.php index a88d01f0..9c112a7f 100644 --- a/Twig/Visitor/DefaultApplyingNodeVisitor.php +++ b/Twig/Visitor/DefaultApplyingNodeVisitor.php @@ -59,7 +59,7 @@ public function doEnterNode(Node $node, Environment $env): Node } if (!$transNode instanceof FilterExpression) { - throw new \RuntimeException(\sprintf('The "desc" filter must be applied after a "trans", or "transchoice" filter.')); + throw new \RuntimeException('The "desc" filter must be applied after a "trans", or "transchoice" filter.'); } $wrappingNode = $node->getNode('node'); diff --git a/composer.json b/composer.json index 57309126..65f822e1 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ } ], "require": { - "php": "^7.1", + "php": "^7.2", "symfony/framework-bundle": "^3.4 || ^4.3 || ^5.0", "symfony/validator": "^3.4 || ^4.3 || ^5.0", "symfony/translation": "^3.4 || ^4.3 || ^5.0", @@ -24,7 +24,8 @@ "twig/twig": "^2.11 || ^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4 || ^5.0", + "symfony/phpunit-bridge": "^5.2", + "bamarni/composer-bin-plugin": "^1.3", "php-translation/translator": "^1.0", "php-http/curl-client": "^1.7", "php-http/message": "^1.6", @@ -37,8 +38,7 @@ "matthiasnoback/symfony-dependency-injection-test": "^4.1", "matthiasnoback/symfony-config-test": "^4.1", "nyholm/psr7": "^1.1", - "nyholm/symfony-bundle-test": "^1.6.1", - "phpunit/phpunit": "^8.4" + "nyholm/symfony-bundle-test": "^1.6.1" }, "conflict": { "symfony/config": "<3.4.31" diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index be1f1d6d..2dd6a23d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -50,16 +50,6 @@ parameters: count: 1 path: DependencyInjection/Configuration.php - - - message: "#^Class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\PostResponseEvent not found\\.$#" - count: 2 - path: EventListener/AutoAddMissingTranslations.php - - - - message: "#^Class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\FilterResponseEvent not found\\.$#" - count: 2 - path: EventListener/EditInPlaceResponseListener.php - - message: "#^Class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface not found\\.$#" count: 1 diff --git a/psalm.baseline.xml b/psalm.baseline.xml new file mode 100644 index 00000000..267f5f29 --- /dev/null +++ b/psalm.baseline.xml @@ -0,0 +1,136 @@ + + + + + !\is_array($source) + !\is_array($target) + + + + + getKernel + + + + + $messages + $messages->getValue(true) + + + array + + + $messages->getValue(true) + + + + + Intl::getLocaleBundle() + + + + + \array_keys($bundles) + + + fixXmlConfig + root + + + + + $container->getParameter('kernel.project_dir') + + + + + PostResponseEvent + + + + + FilterResponseEvent + + + + + LegacyTranslatorInterface + + + + + getNodeVisitors + + + + + $this->translator + $this->translator + + + $this->translator + $this->translator + $this->translator + $this->translator + $this->translator + LegacyTranslatorInterface|NewTranslatorInterface + LegacyTranslatorInterface|NewTranslatorInterface + + + getCatalogue + getLocale + getLocale + setLocale + transChoice + + + + + (string) $id + + + $this->symfonyTranslator + $this->symfonyTranslator + $this->symfonyTranslator + $this->symfonyTranslator + $this->symfonyTranslator + $this->symfonyTranslator + $this->symfonyTranslator + LegacyTranslatorInterface|NewTranslatorInterface + LegacyTranslatorInterface|NewTranslatorInterface + + + getCatalogue + getLocale + setLocale + transChoice + transChoice + + + + + + + + [$this->extension, 'transchoice'] + + + + + LegacyTranslatorInterface + + + transChoice + transChoice + + + + + 0 + 0 + 0 + 0 + 0 + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 00000000..669678fa --- /dev/null +++ b/psalm.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor-bin/php-cs-fixer/composer.json b/vendor-bin/php-cs-fixer/composer.json new file mode 100644 index 00000000..3adf5f5c --- /dev/null +++ b/vendor-bin/php-cs-fixer/composer.json @@ -0,0 +1,9 @@ +{ + "require": { + "php": "^7.2.5 || ^8.0", + "friendsofphp/php-cs-fixer": "2.18.3" + }, + "config": { + "preferred-install": "dist" + } +} diff --git a/vendor-bin/phpstan/composer.json b/vendor-bin/phpstan/composer.json new file mode 100644 index 00000000..bfbc7273 --- /dev/null +++ b/vendor-bin/phpstan/composer.json @@ -0,0 +1,10 @@ +{ + "require": { + "php": "^7.2.5 || ^8.0", + "phpstan/phpstan": "0.12.81", + "phpstan/phpstan-deprecation-rules": "0.12.6" + }, + "config": { + "preferred-install": "dist" + } +} diff --git a/vendor-bin/psalm/composer.json b/vendor-bin/psalm/composer.json new file mode 100644 index 00000000..611c2942 --- /dev/null +++ b/vendor-bin/psalm/composer.json @@ -0,0 +1,9 @@ +{ + "require": { + "php": "^7.2.5 || ^8.0", + "vimeo/psalm": "4.6.4" + }, + "config": { + "preferred-install": "dist" + } +} From b74f88ea35e55d866a71b30105383be33c9efa14 Mon Sep 17 00:00:00 2001 From: Thierry T <1940947+lepiaf@users.noreply.github.com> Date: Fri, 26 Mar 2021 19:28:57 +0100 Subject: [PATCH 174/234] Add support for PHP 8 (#424) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for PHP 8 * Fix phpstan * Exclude Translator/TranslatorInterface.php from phpstan analysis I don't know how to manage this error ``` % make phpstan 2 ↵ ✹ Note: Using configuration file /project/phpstan.neon.dist. 49/49 [â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“] 100% ------ ---------------------------------------------------------------------- Line Translator/TranslatorInterface.php ------ ---------------------------------------------------------------------- 31 Interface Translation\Bundle\Translator\TranslatorInterface extends unknown interface Symfony\Component\Translation\TranslatorInterface. 💡 Learn more at https://phpstan.org/user-guide/discovering-symbols ------ ---------------------------------------------------------------------- -- ------------------------------------------------------------------------- Error -- ------------------------------------------------------------------------- Error message "Interface Translation\Bundle\Translator\TranslatorInterface extends unknown interface Symfony\Component\Translation\TranslatorInterface." cannot be ignored, use excludes_analyse instead. -- ------------------------------------------------------------------------- ``` * Increase lowest version Co-authored-by: Nyholm --- composer.json | 30 +++++++++++++++--------------- phpstan-baseline.neon | 2 ++ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index 65f822e1..27ae5669 100644 --- a/composer.json +++ b/composer.json @@ -10,31 +10,31 @@ } ], "require": { - "php": "^7.2", - "symfony/framework-bundle": "^3.4 || ^4.3 || ^5.0", - "symfony/validator": "^3.4 || ^4.3 || ^5.0", - "symfony/translation": "^3.4 || ^4.3 || ^5.0", - "symfony/twig-bundle": "^3.4 || ^4.3 || ^5.0", - "symfony/finder": "^3.4 || ^4.3 || ^5.0", - "symfony/intl": "^3.4 || ^4.3 || ^5.0", + "php": "^7.2 || ^8.0", + "symfony/framework-bundle": "^3.4.47 || ^4.4.20 || ^5.2.5", + "symfony/validator": "^3.4.47 || ^4.4.20 || ^5.2.5", + "symfony/translation": "^3.4.47 || ^4.4.20 || ^5.2.5", + "symfony/twig-bundle": "^3.4.47 || ^4.4.20 || ^5.2.5", + "symfony/finder": "^3.4.47 || ^4.4.20 || ^5.2.5", + "symfony/intl": "^3.4.47 || ^4.4.20 || ^5.2.5", "php-translation/symfony-storage": "^2.1", "php-translation/extractor": "^2.0", "nyholm/nsa": "^1.1", - "twig/twig": "^2.11 || ^3.0" + "twig/twig": "^2.14.4 || ^3.3" }, "require-dev": { "symfony/phpunit-bridge": "^5.2", "bamarni/composer-bin-plugin": "^1.3", "php-translation/translator": "^1.0", - "php-http/curl-client": "^1.7", - "php-http/message": "^1.6", + "php-http/curl-client": "^1.7 || ^2.0", + "php-http/message": "^1.11", "php-http/message-factory": "^1.0.2", - "symfony/console": "^3.4 || ^4.3 || ^5.0", - "symfony/twig-bridge": "^3.4 || ^4.3 || ^5.0", - "symfony/asset": "^3.4 || ^4.3 || ^5.0", - "symfony/dependency-injection": "^3.4 || ^4.3 || ^5.0", - "symfony/web-profiler-bundle": "^3.4 || ^4.3 || ^5.0", + "symfony/console": "^3.4.47 || ^4.4.20 || ^5.2.5", + "symfony/twig-bridge": "^3.4.47 || ^4.4.20 || ^5.2.5", + "symfony/asset": "^3.4.47 || ^4.4.20 || ^5.2.5", + "symfony/dependency-injection": "^3.4.47 || ^4.4.20 || ^5.2.5", + "symfony/web-profiler-bundle": "^3.4.47 || ^4.4.20 || ^5.2.5", "matthiasnoback/symfony-dependency-injection-test": "^4.1", "matthiasnoback/symfony-config-test": "^4.1", "nyholm/psr7": "^1.1", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2dd6a23d..88381d01 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -145,3 +145,5 @@ parameters: count: 2 path: Twig/TranslationExtension.php + excludes_analyse: + - Translator/TranslatorInterface.php From d0f17d7243fcdcfc8a1080f9426322ae54bc0401 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Fri, 26 Mar 2021 19:42:59 +0100 Subject: [PATCH 175/234] Drop Symfony 3 (#430) --- .github/workflows/ci.yml | 2 -- composer.json | 25 +++++++++++-------------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6411bee..8e54b88d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,8 +16,6 @@ jobs: include: - php: 7.4 strategy: 'lowest' - - php: 7.3 - sf_version: '3.*' - php: 7.3 sf_version: '4.*' diff --git a/composer.json b/composer.json index 27ae5669..a6a0f2e4 100644 --- a/composer.json +++ b/composer.json @@ -11,12 +11,12 @@ ], "require": { "php": "^7.2 || ^8.0", - "symfony/framework-bundle": "^3.4.47 || ^4.4.20 || ^5.2.5", - "symfony/validator": "^3.4.47 || ^4.4.20 || ^5.2.5", - "symfony/translation": "^3.4.47 || ^4.4.20 || ^5.2.5", - "symfony/twig-bundle": "^3.4.47 || ^4.4.20 || ^5.2.5", - "symfony/finder": "^3.4.47 || ^4.4.20 || ^5.2.5", - "symfony/intl": "^3.4.47 || ^4.4.20 || ^5.2.5", + "symfony/framework-bundle": "^4.4.20 || ^5.2.5", + "symfony/validator": "^4.4.20 || ^5.2.5", + "symfony/translation": "^4.4.20 || ^5.2.5", + "symfony/twig-bundle": "^4.4.20 || ^5.2.5", + "symfony/finder": "^4.4.20 || ^5.2.5", + "symfony/intl": "^4.4.20 || ^5.2.5", "php-translation/symfony-storage": "^2.1", "php-translation/extractor": "^2.0", @@ -30,19 +30,16 @@ "php-http/curl-client": "^1.7 || ^2.0", "php-http/message": "^1.11", "php-http/message-factory": "^1.0.2", - "symfony/console": "^3.4.47 || ^4.4.20 || ^5.2.5", - "symfony/twig-bridge": "^3.4.47 || ^4.4.20 || ^5.2.5", - "symfony/asset": "^3.4.47 || ^4.4.20 || ^5.2.5", - "symfony/dependency-injection": "^3.4.47 || ^4.4.20 || ^5.2.5", - "symfony/web-profiler-bundle": "^3.4.47 || ^4.4.20 || ^5.2.5", + "symfony/console": "^4.4.20 || ^5.2.5", + "symfony/twig-bridge": "^4.4.20 || ^5.2.5", + "symfony/asset": "^4.4.20 || ^5.2.5", + "symfony/dependency-injection": "^4.4.20 || ^5.2.5", + "symfony/web-profiler-bundle": "^4.4.20 || ^5.2.5", "matthiasnoback/symfony-dependency-injection-test": "^4.1", "matthiasnoback/symfony-config-test": "^4.1", "nyholm/psr7": "^1.1", "nyholm/symfony-bundle-test": "^1.6.1" }, - "conflict": { - "symfony/config": "<3.4.31" - }, "suggest": { "php-http/httplug-bundle": "To easier configure your httplug clients." }, From 70bb0f610a082ebd3362a344822fae78b1baf76c Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Fri, 26 Mar 2021 21:02:46 +0200 Subject: [PATCH 176/234] Prepare release 0.12.2 (#418) * Prepare release 0.12.2 * Fix order of commands in yaml definition * Updated changelog Co-authored-by: Nyholm --- Changelog.md | 12 +++++++++++- Resources/config/console.yaml | 8 ++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Changelog.md b/Changelog.md index 7f72c61b..a3ea1ed9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,10 +2,20 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. -## Unreleased +## 0.12.2 + +### Added + +- Support for PHP 8 + +### Fixed + +- Add missing command definition #412 +- Fix typo in Tests/Functional/app/config/framework.yaml #413 ### Removed +- Support for Symfony 3 - Support for PHP 7.1 ## 0.12.1 diff --git a/Resources/config/console.yaml b/Resources/config/console.yaml index 88b82e48..dcbf06ec 100644 --- a/Resources/config/console.yaml +++ b/Resources/config/console.yaml @@ -9,7 +9,7 @@ services: tags: - { name: console.command, command: translation:check-missing } - Translation\Bundle\Command\DeleteObsoleteCommand: + Translation\Bundle\Command\DeleteEmptyCommand: public: true arguments: - '@Translation\Bundle\Service\StorageManager' @@ -17,9 +17,9 @@ services: - '@Translation\Bundle\Catalogue\CatalogueManager' - '@Translation\Bundle\Catalogue\CatalogueFetcher' tags: - - { name: console.command, command: translation:delete-obsolete } + - { name: console.command, command: translation:delete-empty } - Translation\Bundle\Command\DeleteEmptyCommand: + Translation\Bundle\Command\DeleteObsoleteCommand: public: true arguments: - '@Translation\Bundle\Service\StorageManager' @@ -27,7 +27,7 @@ services: - '@Translation\Bundle\Catalogue\CatalogueManager' - '@Translation\Bundle\Catalogue\CatalogueFetcher' tags: - - { name: console.command, command: translation:delete-empty } + - { name: console.command, command: translation:delete-obsolete } Translation\Bundle\Command\DownloadCommand: public: true From 31c841ce18b60d1d28592fa21a8a78d0ff4117c0 Mon Sep 17 00:00:00 2001 From: Bjorn Post Date: Fri, 26 Mar 2021 21:05:52 +0100 Subject: [PATCH 177/234] fix: do not require unreleased packages (#433) * fix: do not require unreleased packages * fix: do not require unreleased packages --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index a6a0f2e4..458a5c58 100644 --- a/composer.json +++ b/composer.json @@ -14,9 +14,9 @@ "symfony/framework-bundle": "^4.4.20 || ^5.2.5", "symfony/validator": "^4.4.20 || ^5.2.5", "symfony/translation": "^4.4.20 || ^5.2.5", - "symfony/twig-bundle": "^4.4.20 || ^5.2.5", - "symfony/finder": "^4.4.20 || ^5.2.5", - "symfony/intl": "^4.4.20 || ^5.2.5", + "symfony/twig-bundle": "^4.4.20 || ^5.2.4", + "symfony/finder": "^4.4.20 || ^5.2.4", + "symfony/intl": "^4.4.20 || ^5.2.4", "php-translation/symfony-storage": "^2.1", "php-translation/extractor": "^2.0", @@ -32,9 +32,9 @@ "php-http/message-factory": "^1.0.2", "symfony/console": "^4.4.20 || ^5.2.5", "symfony/twig-bridge": "^4.4.20 || ^5.2.5", - "symfony/asset": "^4.4.20 || ^5.2.5", + "symfony/asset": "^4.4.20 || ^5.2.4", "symfony/dependency-injection": "^4.4.20 || ^5.2.5", - "symfony/web-profiler-bundle": "^4.4.20 || ^5.2.5", + "symfony/web-profiler-bundle": "^4.4.20 || ^5.2.4", "matthiasnoback/symfony-dependency-injection-test": "^4.1", "matthiasnoback/symfony-config-test": "^4.1", "nyholm/psr7": "^1.1", From 9a25b506fd3826301a4a5a0d517e83937448771c Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Fri, 26 Mar 2021 22:06:15 +0200 Subject: [PATCH 178/234] Fix psalm issues (#431) * Typecast ->request->get() value to string * Fix PHP CS Fixer issue * Fix other psalm issues * Fix PHP CS Fixer issues one more time * Try to fix PhpStan issue --- Controller/SymfonyProfilerController.php | 9 +++++---- phpstan-baseline.neon | 5 ----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Controller/SymfonyProfilerController.php b/Controller/SymfonyProfilerController.php index 306cfe2e..3b4f7921 100644 --- a/Controller/SymfonyProfilerController.php +++ b/Controller/SymfonyProfilerController.php @@ -60,7 +60,7 @@ public function editAction(Request $request, string $token): Response } //Assert: This is a POST request - $message->setTranslation($request->request->get('translation')); + $message->setTranslation((string) $request->request->get('translation')); $this->storage->update($message->convertToMessage()); return new Response($message->getTranslation()); @@ -125,7 +125,7 @@ private function getMessage(Request $request, string $token): SfProfilerMessage { $this->profiler->disable(); - $messageId = $request->request->get('message_id', $request->query->get('message_id')); + $messageId = (string) $request->request->get('message_id', $request->query->get('message_id')); $collectorMessages = $this->getMessages($token); @@ -154,8 +154,9 @@ protected function getSelectedMessages(Request $request, string $token): array { $this->profiler->disable(); - $selected = $request->request->get('selected'); - if (!$selected || 0 == \count($selected)) { + /** @var string[] $selected */ + $selected = (array) $request->request->get('selected'); + if (0 === \count($selected)) { return []; } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 88381d01..30f38771 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -25,11 +25,6 @@ parameters: count: 1 path: Command/StatusCommand.php - - - message: "#^Call to an undefined static method Symfony\\\\Component\\\\Intl\\\\Intl\\:\\:getLocaleBundle\\(\\)\\.$#" - count: 1 - path: Controller/WebUIController.php - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\TreeBuilder\\:\\:root\\(\\)\\.$#" count: 1 From ccdf1e2a4acf314488caf608cfb1b7a6191a83ea Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Fri, 26 Mar 2021 22:13:10 +0200 Subject: [PATCH 179/234] Prepare release 0.12.3 --- Changelog.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Changelog.md b/Changelog.md index a3ea1ed9..9a01dc2e 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,13 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.12.3 + +### Fixed + +- Fix psalm issues #431 +- Fix: do not require unreleased packages #433 + ## 0.12.2 ### Added From 6371b34be472410d986282884e0302cdb1f5a279 Mon Sep 17 00:00:00 2001 From: Suvres <67355511+Suvres@users.noreply.github.com> Date: Wed, 26 May 2021 09:05:19 +0200 Subject: [PATCH 180/234] fix extractor parameters (#438) * fix extractor parameters * fix extractor parameters --- Resources/config/extractors.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Resources/config/extractors.yaml b/Resources/config/extractors.yaml index f4247fe8..a692eeca 100644 --- a/Resources/config/extractors.yaml +++ b/Resources/config/extractors.yaml @@ -1,8 +1,4 @@ services: - _defaults: - bind: - $metadataFactory: '@validator' - _instanceof: PhpParser\NodeVisitor: tags: @@ -74,3 +70,6 @@ services: php_translation.extractor.twig.factory.twig: alias: Translation\Extractor\Visitor\Twig\TwigVisitor deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' + + Translation\Extractor\Visitor\Php\Symfony\ValidationAnnotation: + arguments: ['@validator'] From 04c24014f9ff51b9b357eb47dc8a611c74f00e09 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Wed, 9 Jun 2021 16:56:11 +0300 Subject: [PATCH 181/234] Drop SensioLabs (Symfony) insight badge (#441) --- Readme.md | 1 - 1 file changed, 1 deletion(-) diff --git a/Readme.md b/Readme.md index 5fae742e..88dfc44f 100644 --- a/Readme.md +++ b/Readme.md @@ -1,7 +1,6 @@ # Translation Bundle [![Latest Version](https://img.shields.io/github/release/php-translation/symfony-bundle.svg?style=flat-square)](https://github.com/php-translation/symfony-bundle/releases) -[![SensioLabsInsight](https://insight.sensiolabs.com/projects/c289ebe2-41c4-429f-afba-de2f905b9bdb/mini.png)](https://insight.sensiolabs.com/projects/c289ebe2-41c4-429f-afba-de2f905b9bdb) [![Total Downloads](https://img.shields.io/packagist/dt/php-translation/symfony-bundle.svg?style=flat-square)](https://packagist.org/packages/php-translation/symfony-bundle) **Symfony integration for PHP Translation** From 44fb6067837a99f1e19f5a5faf4bdff8ef1c1370 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Thu, 10 Jun 2021 11:28:40 +0300 Subject: [PATCH 182/234] Make Profiler service an optional argument for SymfonyProfilerController (#440) --- Controller/SymfonyProfilerController.php | 47 +++++++++++++++++++----- Resources/config/symfony_profiler.yaml | 3 +- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/Controller/SymfonyProfilerController.php b/Controller/SymfonyProfilerController.php index 3b4f7921..73005424 100644 --- a/Controller/SymfonyProfilerController.php +++ b/Controller/SymfonyProfilerController.php @@ -12,9 +12,11 @@ namespace Translation\Bundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Profiler\Profiler; +use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Translation\DataCollector\TranslationDataCollector; use Symfony\Component\Translation\DataCollectorTranslator; use Symfony\Component\VarDumper\Cloner\Data; @@ -28,13 +30,15 @@ class SymfonyProfilerController extends AbstractController { private $storage; + /** + * @var Profiler An optional dependency + */ private $profiler; private $isToolbarAllowEdit; - public function __construct(StorageService $storage, Profiler $profiler, bool $isToolbarAllowEdit) + public function __construct(StorageService $storage, bool $isToolbarAllowEdit) { $this->storage = $storage; - $this->profiler = $profiler; $this->isToolbarAllowEdit = $isToolbarAllowEdit; } @@ -45,7 +49,7 @@ public function editAction(Request $request, string $token): Response } if (!$request->isXmlHttpRequest()) { - return $this->redirectToRoute('_profiler', ['token' => $token]); + return $this->redirectToProfiler($token); } $message = $this->getMessage($request, $token); @@ -69,7 +73,7 @@ public function editAction(Request $request, string $token): Response public function syncAction(Request $request, string $token): Response { if (!$request->isXmlHttpRequest()) { - return $this->redirectToRoute('_profiler', ['token' => $token]); + return $this->redirectToProfiler($token); } $sfMessage = $this->getMessage($request, $token); @@ -88,7 +92,7 @@ public function syncAction(Request $request, string $token): Response public function syncAllAction(Request $request, string $token): Response { if (!$request->isXmlHttpRequest()) { - return $this->redirectToRoute('_profiler', ['token' => $token]); + return $this->redirectToProfiler($token); } $this->storage->sync(); @@ -104,7 +108,7 @@ public function syncAllAction(Request $request, string $token): Response public function createAssetsAction(Request $request, string $token): Response { if (!$request->isXmlHttpRequest()) { - return $this->redirectToRoute('_profiler', ['token' => $token]); + return $this->redirectToProfiler($token); } $messages = $this->getSelectedMessages($request, $token); @@ -123,7 +127,7 @@ public function createAssetsAction(Request $request, string $token): Response private function getMessage(Request $request, string $token): SfProfilerMessage { - $this->profiler->disable(); + $this->getProfiler()->disable(); $messageId = (string) $request->request->get('message_id', $request->query->get('message_id')); @@ -136,7 +140,7 @@ private function getMessage(Request $request, string $token): SfProfilerMessage if (DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK === $message->getState()) { /** @var \Symfony\Component\HttpKernel\DataCollector\RequestDataCollector */ - $requestCollector = $this->profiler->loadProfile($token)->getCollector('request'); + $requestCollector = $this->getProfiler()->loadProfile($token)->getCollector('request'); $message ->setLocale($requestCollector->getLocale()) @@ -152,7 +156,7 @@ private function getMessage(Request $request, string $token): SfProfilerMessage */ protected function getSelectedMessages(Request $request, string $token): array { - $this->profiler->disable(); + $this->getProfiler()->disable(); /** @var string[] $selected */ $selected = (array) $request->request->get('selected'); @@ -172,7 +176,7 @@ protected function getSelectedMessages(Request $request, string $token): array private function getMessages(string $token, string $profileName = 'translation'): array { - $profile = $this->profiler->loadProfile($token); + $profile = $this->getProfiler()->loadProfile($token); if (null === $dataCollector = $profile->getCollector($profileName)) { throw $this->createNotFoundException("No collector with name \"$profileName\" was found."); @@ -189,4 +193,27 @@ private function getMessages(string $token, string $profileName = 'translation') return $messages; } + + public function setProfiler(Profiler $profiler): void + { + $this->profiler = $profiler; + } + + private function getProfiler(): Profiler + { + if (!$this->profiler) { + throw new \Exception('The "profiler" service is missing. Please, run "composer require symfony/web-profiler-bundle" first to use this feature.'); + } + + return $this->profiler; + } + + private function redirectToProfiler(string $token): RedirectResponse + { + try { + return $this->redirectToRoute('_profiler', ['token' => $token]); + } catch (RouteNotFoundException $e) { + throw new \Exception('Route to profiler page not found. Please, run "composer require symfony/web-profiler-bundle" first to use this feature.'); + } + } } diff --git a/Resources/config/symfony_profiler.yaml b/Resources/config/symfony_profiler.yaml index 7420a6d5..4deb418d 100644 --- a/Resources/config/symfony_profiler.yaml +++ b/Resources/config/symfony_profiler.yaml @@ -11,5 +11,6 @@ services: tags: ['container.service_subscriber'] arguments: - '@Translation\Bundle\Service\StorageService' - - '@profiler' - '%php_translation.toolbar.allow_edit%' + calls: + - setProfiler: ['@?profiler'] From f80167dec0d7a480085cf172eddb95ebcf89ed8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Caselles?= Date: Tue, 15 Jun 2021 22:54:25 +0200 Subject: [PATCH 183/234] Update ExtractCommand.php (#435) If translating collaboratively translation results sort differs from each developer that runs the command. --- Command/ExtractCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Command/ExtractCommand.php b/Command/ExtractCommand.php index 5000c362..98dba731 100644 --- a/Command/ExtractCommand.php +++ b/Command/ExtractCommand.php @@ -139,6 +139,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function getConfiguredFinder(Configuration $config): Finder { $finder = new Finder(); + $finder->sortByName(); $finder->in($config->getDirs()); foreach ($config->getExcludedDirs() as $exclude) { From d7174d9216af0966c01a0a79f21a00a82e0fd43a Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Wed, 16 Jun 2021 22:43:37 +0300 Subject: [PATCH 184/234] Fix tests on Symfony 5.3 (#443) * Enable assets in test config * Require symfony/asset as a main dependency * Revert back constraints for symfony/asset * Ignore legacy code in PHPStan * Revert "Enable assets in test config" This reverts commit c2f8d05b7c33ad75688238d348d00d0f5b958580. --- composer.json | 4 ++-- phpstan.neon.dist | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 458a5c58..d9a3be8d 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ "php-translation/symfony-storage": "^2.1", "php-translation/extractor": "^2.0", "nyholm/nsa": "^1.1", - "twig/twig": "^2.14.4 || ^3.3" + "twig/twig": "^2.14.4 || ^3.3", + "symfony/asset": "^4.4.20 || ^5.2.4" }, "require-dev": { "symfony/phpunit-bridge": "^5.2", @@ -32,7 +33,6 @@ "php-http/message-factory": "^1.0.2", "symfony/console": "^4.4.20 || ^5.2.5", "symfony/twig-bridge": "^4.4.20 || ^5.2.5", - "symfony/asset": "^4.4.20 || ^5.2.4", "symfony/dependency-injection": "^4.4.20 || ^5.2.5", "symfony/web-profiler-bundle": "^4.4.20 || ^5.2.4", "matthiasnoback/symfony-dependency-injection-test": "^4.1", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index cdcc6264..5e71d7ec 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -17,3 +17,5 @@ parameters: - Service - Translator - Twig + ignoreErrors: + - '#Call to an undefined static method Symfony\\Component\\Intl\\Intl::getLocaleBundle\(\)\.#' From 84c843d6dcc81c101afbf505171e9afa31210c10 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Thu, 17 Jun 2021 15:04:37 +0300 Subject: [PATCH 185/234] Suppress getMasterRequest() deprecation message (#444) --- Legacy/LegacyHelper.php | 31 ++++++++++++++++++++++++++++ Translator/EditInPlaceTranslator.php | 7 +++++-- Twig/EditInPlaceExtension.php | 5 ++++- 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 Legacy/LegacyHelper.php diff --git a/Legacy/LegacyHelper.php b/Legacy/LegacyHelper.php new file mode 100644 index 00000000..1b334915 --- /dev/null +++ b/Legacy/LegacyHelper.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Legacy; + +use Symfony\Component\HttpFoundation\RequestStack; + +/** + * A legacy helper to suppress deprecations on RequestStack. + * + * @author Victor Bocharsky + */ +class LegacyHelper +{ + public static function getMainRequest(RequestStack $requestStack) + { + if (\method_exists($requestStack, 'getMainRequest')) { + return $requestStack->getMainRequest(); + } + + return $requestStack->getMasterRequest(); + } +} diff --git a/Translator/EditInPlaceTranslator.php b/Translator/EditInPlaceTranslator.php index 1d94e95e..f2966af3 100644 --- a/Translator/EditInPlaceTranslator.php +++ b/Translator/EditInPlaceTranslator.php @@ -18,6 +18,7 @@ use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface as NewTranslatorInterface; use Translation\Bundle\EditInPlace\ActivatorInterface; +use Translation\Bundle\Legacy\LegacyHelper; /** * Custom Translator for HTML rendering only (output `` tags). @@ -75,7 +76,8 @@ public function getCatalogue($locale = null): MessageCatalogueInterface public function trans($id, array $parameters = [], $domain = null, $locale = null): ?string { $original = $this->translator->trans($id, $parameters, $domain, $locale); - if (!$this->activator->checkRequest($this->requestStack->getMasterRequest())) { + $request = LegacyHelper::getMainRequest($this->requestStack); + if (!$this->activator->checkRequest($request)) { return $original; } @@ -105,7 +107,8 @@ public function trans($id, array $parameters = [], $domain = null, $locale = nul */ public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null): ?string { - if (!$this->activator->checkRequest($this->requestStack->getMasterRequest())) { + $request = LegacyHelper::getMainRequest($this->requestStack); + if (!$this->activator->checkRequest($request)) { return $this->translator->transChoice($id, $number, $parameters, $domain, $locale); } diff --git a/Twig/EditInPlaceExtension.php b/Twig/EditInPlaceExtension.php index f86a571e..9a6ba66d 100644 --- a/Twig/EditInPlaceExtension.php +++ b/Twig/EditInPlaceExtension.php @@ -14,6 +14,7 @@ use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Component\HttpFoundation\RequestStack; use Translation\Bundle\EditInPlace\ActivatorInterface; +use Translation\Bundle\Legacy\LegacyHelper; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; @@ -52,7 +53,9 @@ public function getFilters(): array */ public function isSafe($node): array { - return $this->activator->checkRequest($this->requestStack->getMasterRequest()) ? ['html'] : []; + $request = LegacyHelper::getMainRequest($this->requestStack); + + return $this->activator->checkRequest($request) ? ['html'] : []; } /** From ad9c5f5b655e3dbe92c072499497a22d75bb4fa3 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Fri, 18 Jun 2021 23:29:50 +0300 Subject: [PATCH 186/234] Suppress deprecations on deprecating legacy services (#445) * Suppress deprecations on deprecating legacy services * Apply PHP CS Fixer recommendations * Fix the Kernel version ID number * Simplify code: Remove unnecessary var * Rename the method to registerDeprecatedServices() --- DependencyInjection/TranslationExtension.php | 5 +++ Legacy/LegacyHelper.php | 28 ++++++++++++ Resources/config/console.yaml | 17 ------- Resources/config/edit_in_place.yaml | 14 ------ Resources/config/extractors.yaml | 47 -------------------- Resources/config/legacy/console.php | 20 +++++++++ Resources/config/legacy/edit_in_place.php | 18 ++++++++ Resources/config/legacy/extractors.php | 40 +++++++++++++++++ Resources/config/legacy/services.php | 30 +++++++++++++ Resources/config/services.yaml | 40 ----------------- 10 files changed, 141 insertions(+), 118 deletions(-) create mode 100644 Resources/config/legacy/console.php create mode 100644 Resources/config/legacy/edit_in_place.php create mode 100644 Resources/config/legacy/extractors.php create mode 100644 Resources/config/legacy/services.php diff --git a/DependencyInjection/TranslationExtension.php b/DependencyInjection/TranslationExtension.php index 11ed972e..7fdb0582 100644 --- a/DependencyInjection/TranslationExtension.php +++ b/DependencyInjection/TranslationExtension.php @@ -47,9 +47,12 @@ public function load(array $configs, ContainerBuilder $container): void $configuration = new Configuration($container); $config = $this->processConfiguration($configuration, $configs); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $legacyLoader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config/legacy')); $loader->load('services.yaml'); + $legacyLoader->load('services.php'); $loader->load('extractors.yaml'); + $legacyLoader->load('extractors.php'); // Add major version to extractor $container->getDefinition(FormTypeChoices::class) @@ -74,6 +77,7 @@ public function load(array $configs, ContainerBuilder $container): void if ($config['edit_in_place']['enabled']) { $loader->load('edit_in_place.yaml'); + $legacyLoader->load('edit_in_place.php'); $this->enableEditInPlace($container, $config); } @@ -89,6 +93,7 @@ public function load(array $configs, ContainerBuilder $container): void } $loader->load('console.yaml'); + $legacyLoader->load('console.php'); } /** diff --git a/Legacy/LegacyHelper.php b/Legacy/LegacyHelper.php index 1b334915..c59cba6d 100644 --- a/Legacy/LegacyHelper.php +++ b/Legacy/LegacyHelper.php @@ -11,7 +11,9 @@ namespace Translation\Bundle\Legacy; +use Symfony\Component\DependencyInjection\Loader\Configurator\ServicesConfigurator; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\Kernel; /** * A legacy helper to suppress deprecations on RequestStack. @@ -28,4 +30,30 @@ public static function getMainRequest(RequestStack $requestStack) return $requestStack->getMasterRequest(); } + + /** + * @param array[string $id, string $parent, ?bool $isPublic] $legacyServices + */ + public static function registerDeprecatedServices(ServicesConfigurator $servicesConfigurator, array $legacyServices) + { + foreach ($legacyServices as $legacyService) { + $id = $legacyService[0]; + $parent = $legacyService[1]; + $isPublic = $legacyService[2] ?? false; + + // Declare legacy services to remove in next major release + $service = $servicesConfigurator->set($id) + ->parent($parent); + + if (Kernel::VERSION_ID < 50100) { + $service->deprecate('Since php-translation/symfony-bundle 0.10.0: The "%service_id%" service is deprecated. You should stop using it, as it will soon be removed.'); + } else { + $service->deprecate('php-translation/symfony-bundle', '0.10.0', 'The "%service_id%" service is deprecated. You should stop using it, as it will soon be removed.'); + } + + if ($isPublic) { + $service->public(); + } + } + } } diff --git a/Resources/config/console.yaml b/Resources/config/console.yaml index dcbf06ec..2a003472 100644 --- a/Resources/config/console.yaml +++ b/Resources/config/console.yaml @@ -65,20 +65,3 @@ services: - '@Translation\Bundle\Service\StorageManager' tags: - { name: console.command, command: translation:sync } - - # To remove in next major release - php_translator.console.delete_obsolete: - parent: Translation\Bundle\Command\DeleteObsoleteCommand - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translator.console.download: - parent: Translation\Bundle\Command\DownloadCommand - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translator.console.extract: - parent: Translation\Bundle\Command\ExtractCommand - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translator.console.status: - parent: Translation\Bundle\Command\StatusCommand - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translator.console.sync: - parent: Translation\Bundle\Command\SyncCommand - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' diff --git a/Resources/config/edit_in_place.yaml b/Resources/config/edit_in_place.yaml index f6180e55..47f4220e 100644 --- a/Resources/config/edit_in_place.yaml +++ b/Resources/config/edit_in_place.yaml @@ -36,17 +36,3 @@ services: - ~ tags: - { name: 'twig.extension' } - - # To remove in next major release - php_translation.edit_in_place.response_listener: - parent: Translation\Bundle\EventListener\EditInPlaceResponseListener - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.edit_in_place.activator: - parent: Translation\Bundle\EditInPlace\Activator - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translator.edit_in_place.xtrans_html_translator: - parent: Translation\Bundle\Translator\EditInPlaceTranslator - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.edit_in_place.extension.trans: - parent: Translation\Bundle\Twig\EditInPlaceExtension - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' diff --git a/Resources/config/extractors.yaml b/Resources/config/extractors.yaml index a692eeca..68bb9095 100644 --- a/Resources/config/extractors.yaml +++ b/Resources/config/extractors.yaml @@ -24,52 +24,5 @@ services: tags: - { name: 'php_translation.visitor', type: 'twig' } - # To remove in next major release - php_translation.extractor.php: - alias: Translation\Extractor\FileExtractor\PHPFileExtractor - deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' - php_translation.extractor.twig: - alias: Translation\Extractor\FileExtractor\TwigFileExtractor - deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' - php_translation.extractor.php.visitor.ContainerAwareTrans: - alias: Translation\Extractor\Visitor\Php\Symfony\ContainerAwareTrans - deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' - php_translation.extractor.php.visitor.ContainerAwareTransChoice: - alias: Translation\Extractor\Visitor\Php\Symfony\ContainerAwareTransChoice - deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' - php_translation.extractor.php.visitor.FlashMessage: - alias: Translation\Extractor\Visitor\Php\Symfony\FlashMessage - deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' - php_translation.extractor.php.visitor.FormTypeChoices: - alias: Translation\Extractor\Visitor\Php\Symfony\FormTypeChoices - deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' - php_translation.extractor.php.visitor.FormTypeEmptyValue: - alias: Translation\Extractor\Visitor\Php\Symfony\FormTypeEmptyValue - deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' - php_translation.extractor.php.visitor.FormTypeHelp: - alias: Translation\Extractor\Visitor\Php\Symfony\FormTypeHelp - deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' - php_translation.extractor.php.visitor.FormTypeInvalidMessage: - alias: Translation\Extractor\Visitor\Php\Symfony\FormTypeInvalidMessage - deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' - php_translation.extractor.php.visitor.FormTypeLabelExplicit: - alias: Translation\Extractor\Visitor\Php\Symfony\FormTypeLabelExplicit - deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' - php_translation.extractor.php.visitor.FormTypeLabelImplicit: - alias: Translation\Extractor\Visitor\Php\Symfony\FormTypeLabelImplicit - deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' - php_translation.extractor.php.visitor.FormTypePlaceholder: - alias: Translation\Extractor\Visitor\Php\Symfony\FormTypePlaceholder - deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' - php_translation.extractor.php.visitor.ValidationAnnotation: - alias: Translation\Extractor\Visitor\Php\Symfony\ValidationAnnotation - deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' - php_translation.extractor.php.visitor.SourceLocationContainerVisitor: - alias: Translation\Extractor\Visitor\Php\SourceLocationContainerVisitor - deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' - php_translation.extractor.twig.factory.twig: - alias: Translation\Extractor\Visitor\Twig\TwigVisitor - deprecated: 'The "%service_id%" service is deprecated. You should use "%alias_id%" instead, as it will be removed in the future.' - Translation\Extractor\Visitor\Php\Symfony\ValidationAnnotation: arguments: ['@validator'] diff --git a/Resources/config/legacy/console.php b/Resources/config/legacy/console.php new file mode 100644 index 00000000..568b10e8 --- /dev/null +++ b/Resources/config/legacy/console.php @@ -0,0 +1,20 @@ +services(), [ + ['php_translator.console.delete_obsolete', DeleteObsoleteCommand::class], + ['php_translator.console.download', DownloadCommand::class], + ['php_translator.console.extract', ExtractCommand::class], + ['php_translator.console.status', StatusCommand::class], + ['php_translator.console.sync', SyncCommand::class], + ]); +}; diff --git a/Resources/config/legacy/edit_in_place.php b/Resources/config/legacy/edit_in_place.php new file mode 100644 index 00000000..92127954 --- /dev/null +++ b/Resources/config/legacy/edit_in_place.php @@ -0,0 +1,18 @@ +services(), [ + ['php_translation.edit_in_place.response_listener', EditInPlaceResponseListener::class], + ['php_translation.edit_in_place.activator', Activator::class], + ['php_translator.edit_in_place.xtrans_html_translator', EditInPlaceTranslator::class], + ['php_translation.edit_in_place.extension.trans', EditInPlaceExtension::class], + ]); +}; diff --git a/Resources/config/legacy/extractors.php b/Resources/config/legacy/extractors.php new file mode 100644 index 00000000..687b4b9d --- /dev/null +++ b/Resources/config/legacy/extractors.php @@ -0,0 +1,40 @@ +services(), [ + ['php_translation.extractor.php', PHPFileExtractor::class], + ['php_translation.extractor.twig', TwigFileExtractor::class], + ['php_translation.extractor.php.visitor.ContainerAwareTrans', ContainerAwareTrans::class], + ['php_translation.extractor.php.visitor.ContainerAwareTransChoice', ContainerAwareTransChoice::class], + ['php_translation.extractor.php.visitor.FlashMessage', FlashMessage::class], + ['php_translation.extractor.php.visitor.FormTypeChoices', FormTypeChoices::class], + ['php_translation.extractor.php.visitor.FormTypeEmptyValue', FormTypeEmptyValue::class], + ['php_translation.extractor.php.visitor.FormTypeHelp', FormTypeHelp::class], + ['php_translation.extractor.php.visitor.FormTypeInvalidMessage', FormTypeInvalidMessage::class], + ['php_translation.extractor.php.visitor.FormTypeLabelExplicit', FormTypeLabelExplicit::class], + ['php_translation.extractor.php.visitor.FormTypeLabelImplicit', FormTypeLabelImplicit::class], + ['php_translation.extractor.php.visitor.FormTypePlaceholder', FormTypePlaceholder::class], + ['php_translation.extractor.php.visitor.ValidationAnnotation', ValidationAnnotation::class], + ['php_translation.extractor.php.visitor.SourceLocationContainerVisitor', SourceLocationContainerVisitor::class], + ['php_translation.extractor.twig.factory.twig', TwigVisitor::class], + ]); +}; diff --git a/Resources/config/legacy/services.php b/Resources/config/legacy/services.php new file mode 100644 index 00000000..9daaf626 --- /dev/null +++ b/Resources/config/legacy/services.php @@ -0,0 +1,30 @@ +services(), [ + ['php_translation.catalogue_fetcher', CatalogueFetcher::class, true], + ['php_translation.catalogue_writer', CatalogueWriter::class, true], + ['php_translation.catalogue_manager', CatalogueManager::class, true], + ['php_translation.extractor', Extractor::class], + ['php_translation.storage_manager', StorageManager::class, true], + ['php_translation.configuration_manager', ConfigurationManager::class, true], + ['php_translation.importer', Importer::class, true], + ['php_translation.cache_clearer', CacheClearer::class, true], + ['php_translation.catalogue_counter', CatalogueCounter::class, true], + ['php_translation.twig_extension', TranslationExtension::class], + ]); +}; diff --git a/Resources/config/services.yaml b/Resources/config/services.yaml index 23cec40c..646e1acb 100644 --- a/Resources/config/services.yaml +++ b/Resources/config/services.yaml @@ -43,43 +43,3 @@ services: arguments: ['@translator', '%kernel.debug%'] tags: - { name: twig.extension } - - # To remove in next major release - php_translation.catalogue_fetcher: - parent: Translation\Bundle\Catalogue\CatalogueFetcher - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - public: true - php_translation.catalogue_writer: - parent: Translation\Bundle\Catalogue\CatalogueWriter - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - public: true - php_translation.catalogue_manager: - parent: Translation\Bundle\Catalogue\CatalogueManager - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - public: true - php_translation.extractor: - parent: Translation\Extractor\Extractor - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.storage_manager: - parent: Translation\Bundle\Service\StorageManager - public: true - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.configuration_manager: - parent: Translation\Bundle\Service\ConfigurationManager - public: true - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.importer: - parent: Translation\Bundle\Service\Importer - public: true - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.cache_clearer: - parent: Translation\Bundle\Service\CacheClearer - public: true - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.catalogue_counter: - parent: Translation\Bundle\Catalogue\CatalogueCounter - public: true - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' - php_translation.twig_extension: - parent: Translation\Bundle\Twig\TranslationExtension - deprecated: 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future.' From d7aba90c2a6f06ebf34263a5c5e124eb538c74fd Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Tue, 22 Jun 2021 17:30:51 +0300 Subject: [PATCH 187/234] Fix PHPUnit deprecations (#447) --- Tests/Functional/Command/ExtractCommandTest.php | 4 ++-- Tests/Functional/Command/SyncCommandTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/Functional/Command/ExtractCommandTest.php b/Tests/Functional/Command/ExtractCommandTest.php index 786c44f7..1556b849 100644 --- a/Tests/Functional/Command/ExtractCommandTest.php +++ b/Tests/Functional/Command/ExtractCommandTest.php @@ -93,8 +93,8 @@ public function testExecute(): void $output = $commandTester->getDisplay(); // Make sure we have 4 new messages - $this->assertRegExp('|New messages +4|s', $output); - $this->assertRegExp('|Total defined messages +8|s', $output); + $this->assertMatchesRegularExpression('|New messages +4|s', $output); + $this->assertMatchesRegularExpression('|Total defined messages +8|s', $output); $container = $this->getContainer(); $config = $container->get(ConfigurationManager::class)->getConfiguration('app'); diff --git a/Tests/Functional/Command/SyncCommandTest.php b/Tests/Functional/Command/SyncCommandTest.php index b06255fa..d758cbf0 100644 --- a/Tests/Functional/Command/SyncCommandTest.php +++ b/Tests/Functional/Command/SyncCommandTest.php @@ -80,8 +80,8 @@ public function testExecute(): void $this->fail('The command should fail when called with an unknown configuration key.'); } catch (\InvalidArgumentException $e) { - $this->assertRegExp('|Unknown storage "fail"\.|s', $e->getMessage()); - $this->assertRegExp('|Available storages are "app"|s', $e->getMessage()); + $this->assertMatchesRegularExpression('|Unknown storage "fail"\.|s', $e->getMessage()); + $this->assertMatchesRegularExpression('|Available storages are "app"|s', $e->getMessage()); } } } From e8cdfebd50d34a332524d4232a0acc7865732819 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Thu, 20 Jan 2022 10:33:10 +0200 Subject: [PATCH 188/234] Fix broken tests (#458) * Fix tests: Lock php-http/httplug on latest v1 * Do not allow nyholm/symfony-bundle-test >=v1.8 that causes tests to fail * Remove php-http/httplug as a direct dependency --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d9a3be8d..5d6a2f59 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "matthiasnoback/symfony-dependency-injection-test": "^4.1", "matthiasnoback/symfony-config-test": "^4.1", "nyholm/psr7": "^1.1", - "nyholm/symfony-bundle-test": "^1.6.1" + "nyholm/symfony-bundle-test": "^1.6.1, <1.8" }, "suggest": { "php-http/httplug-bundle": "To easier configure your httplug clients." From 92f3de57aa7201e97904a50d53171e7208b66968 Mon Sep 17 00:00:00 2001 From: Axel Guckelsberger Date: Thu, 20 Jan 2022 10:18:24 +0100 Subject: [PATCH 189/234] register visitor tag also for TranslateAnnotationVisitor and Knp visitors (#451) --- Resources/config/extractors.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Resources/config/extractors.yaml b/Resources/config/extractors.yaml index 68bb9095..232f8a39 100644 --- a/Resources/config/extractors.yaml +++ b/Resources/config/extractors.yaml @@ -14,8 +14,8 @@ services: - { name: 'php_translation.extractor', type: 'twig' } # PHP Visitors: - Translation\Extractor\Visitor\Php\Symfony\: - resource: "%extractor_vendor_dir%/Visitor/Php/Symfony/*" + Translation\Extractor\Visitor\Php\: + resource: "%extractor_vendor_dir%/Visitor/Php/*" Translation\Extractor\Visitor\Php\SourceLocationContainerVisitor: ~ From d9d71b137b8f4dca020fa44ce704020a8b2465b3 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Thu, 20 Jan 2022 11:27:04 +0200 Subject: [PATCH 190/234] Prepare release v0.12.4 (#456) * Prepare release v0.12.4 * Update Changelog.md * Include merged #451 PR in the changelog --- Changelog.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Changelog.md b/Changelog.md index 9a01dc2e..17cf2422 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,24 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.12.4 + +### Fixed + +- Fix extractor parameters #438 +- Make Profiler service an optional argument for SymfonyProfilerController #440 +- Update ExtractCommand.php #435 +- Fix tests on Symfony 5.3 #443 +- Suppress getMasterRequest() deprecation message #444 +- Suppress deprecations on deprecating legacy services #445 +- Fix PHPUnit deprecations #447 +- Fix broken tests #458 +- Register visitor tag also for TranslateAnnotationVisitor and Knp visitors #451 + +### Removed + +- Drop SensioLabs (Symfony) insight badge #441 + ## 0.12.3 ### Fixed From 85806a78c1b70f23c1e5b30558512c232d06ac25 Mon Sep 17 00:00:00 2001 From: Gordon Franke Date: Thu, 20 Jan 2022 15:39:59 +0100 Subject: [PATCH 191/234] refac: remove AbstractController from EditInPlaceController (#459) --- Controller/EditInPlaceController.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Controller/EditInPlaceController.php b/Controller/EditInPlaceController.php index 4ee7abf7..32296ce8 100644 --- a/Controller/EditInPlaceController.php +++ b/Controller/EditInPlaceController.php @@ -11,7 +11,6 @@ namespace Translation\Bundle\Controller; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -25,7 +24,7 @@ /** * @author Damien Alexandre */ -class EditInPlaceController extends AbstractController +class EditInPlaceController { private $storageManager; private $cacheClearer; From 874cf437436b7c070a0ae4a752030e78d774ceb3 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Mon, 24 Jan 2022 11:02:19 +0200 Subject: [PATCH 192/234] Stop extending AbstractController to fix some deprecations (#460) * Stop extending AbstractController to fix some deprecations * Remove container.service_subscriber tag --- Controller/SymfonyProfilerController.php | 28 ++++++++++++++++-------- Controller/WebUIController.php | 19 +++++++++++----- Resources/config/symfony_profiler.yaml | 3 ++- Resources/config/webui.yaml | 1 + 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/Controller/SymfonyProfilerController.php b/Controller/SymfonyProfilerController.php index 73005424..cae0e5d9 100644 --- a/Controller/SymfonyProfilerController.php +++ b/Controller/SymfonyProfilerController.php @@ -11,34 +11,40 @@ namespace Translation\Bundle\Controller; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Profiler\Profiler; use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Translation\DataCollector\TranslationDataCollector; use Symfony\Component\Translation\DataCollectorTranslator; use Symfony\Component\VarDumper\Cloner\Data; use Translation\Bundle\Model\SfProfilerMessage; use Translation\Bundle\Service\StorageService; use Translation\Common\Model\MessageInterface; +use Twig\Environment; /** * @author Tobias Nyholm */ -class SymfonyProfilerController extends AbstractController +class SymfonyProfilerController { - private $storage; /** * @var Profiler An optional dependency */ private $profiler; + private $storage; + private $twig; + private $router; private $isToolbarAllowEdit; - public function __construct(StorageService $storage, bool $isToolbarAllowEdit) + public function __construct(StorageService $storage, Environment $twig, RouterInterface $router, bool $isToolbarAllowEdit) { $this->storage = $storage; + $this->twig = $twig; + $this->router = $router; $this->isToolbarAllowEdit = $isToolbarAllowEdit; } @@ -57,10 +63,12 @@ public function editAction(Request $request, string $token): Response if ($request->isMethod('GET')) { $translation = $this->storage->syncAndFetchMessage($message->getLocale(), $message->getDomain(), $message->getKey()); - return $this->render('@Translation/SymfonyProfiler/edit.html.twig', [ + $content = $this->twig->render('@Translation/SymfonyProfiler/edit.html.twig', [ 'message' => $translation, 'key' => $request->query->get('message_id'), ]); + + return new Response($content); } //Assert: This is a POST request @@ -134,7 +142,7 @@ private function getMessage(Request $request, string $token): SfProfilerMessage $collectorMessages = $this->getMessages($token); if (!isset($collectorMessages[$messageId])) { - throw $this->createNotFoundException(\sprintf('No message with key "%s" was found.', $messageId)); + throw new NotFoundHttpException(\sprintf('No message with key "%s" was found.', $messageId)); } $message = SfProfilerMessage::create($collectorMessages[$messageId]); @@ -179,10 +187,10 @@ private function getMessages(string $token, string $profileName = 'translation') $profile = $this->getProfiler()->loadProfile($token); if (null === $dataCollector = $profile->getCollector($profileName)) { - throw $this->createNotFoundException("No collector with name \"$profileName\" was found."); + throw new NotFoundHttpException("No collector with name \"$profileName\" was found."); } if (!$dataCollector instanceof TranslationDataCollector) { - throw $this->createNotFoundException("Collector with name \"$profileName\" is not an instance of TranslationDataCollector."); + throw new NotFoundHttpException("Collector with name \"$profileName\" is not an instance of TranslationDataCollector."); } $messages = $dataCollector->getMessages(); @@ -211,7 +219,9 @@ private function getProfiler(): Profiler private function redirectToProfiler(string $token): RedirectResponse { try { - return $this->redirectToRoute('_profiler', ['token' => $token]); + $targetUrl = $this->router->generate('_profiler', ['token' => $token]); + + return new RedirectResponse($targetUrl); } catch (RouteNotFoundException $e) { throw new \Exception('Route to profiler page not found. Please, run "composer require symfony/web-profiler-bundle" first to use this feature.'); } diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index 4ef7daa7..32d3a2cd 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -11,7 +11,6 @@ namespace Translation\Bundle\Controller; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -29,17 +28,19 @@ use Translation\Common\Exception\StorageException; use Translation\Common\Model\Message; use Translation\Common\Model\MessageInterface; +use Twig\Environment; /** * @author Tobias Nyholm */ -class WebUIController extends AbstractController +class WebUIController { private $configurationManager; private $catalogueFetcher; private $catalogueManager; private $storageManager; private $validator; + private $twig; private $locales; private $isWebUIEnabled; private $isWebUIAllowCreate; @@ -52,6 +53,7 @@ public function __construct( CatalogueManager $catalogueManager, StorageManager $storageManager, ValidatorInterface $validator, + Environment $twig, array $locales, bool $isWebUIEnabled, bool $isWebUIAllowCreate, @@ -63,6 +65,7 @@ public function __construct( $this->catalogueManager = $catalogueManager; $this->storageManager = $storageManager; $this->validator = $validator; + $this->twig = $twig; $this->locales = $locales; $this->isWebUIEnabled = $isWebUIEnabled; $this->isWebUIAllowCreate = $isWebUIAllowCreate; @@ -107,7 +110,7 @@ public function indexAction(?string $configName = null): Response } } - return $this->render('@Translation/WebUI/index.html.twig', [ + $content = $this->twig->render('@Translation/WebUI/index.html.twig', [ 'catalogues' => $catalogues, 'catalogueSize' => $catalogueSize, 'maxDomainSize' => $maxDomainSize, @@ -116,6 +119,8 @@ public function indexAction(?string $configName = null): Response 'configName' => $config->getName(), 'configNames' => $this->configurationManager->getNames(), ]); + + return new Response($content); } /** @@ -137,7 +142,7 @@ public function showAction(string $configName, string $locale, string $domain): return \strcmp($a->getKey(), $b->getKey()); }); - return $this->render('@Translation/WebUI/show.html.twig', [ + $content = $this->twig->render('@Translation/WebUI/show.html.twig', [ 'messages' => $messages, 'domains' => $this->catalogueManager->getDomains(), 'currentDomain' => $domain, @@ -149,6 +154,8 @@ public function showAction(string $configName, string $locale, string $domain): 'allow_delete' => $this->isWebUIAllowDelete, 'file_base_path' => $this->fileBasePath, ]); + + return new Response($content); } public function createAction(Request $request, string $configName, string $locale, string $domain): Response @@ -177,9 +184,11 @@ public function createAction(Request $request, string $configName, string $local return new Response($e->getMessage(), Response::HTTP_BAD_REQUEST); } - return $this->render('@Translation/WebUI/create.html.twig', [ + $content = $this->twig->render('@Translation/WebUI/create.html.twig', [ 'message' => $message, ]); + + return new Response($content); } public function editAction(Request $request, string $configName, string $locale, string $domain): Response diff --git a/Resources/config/symfony_profiler.yaml b/Resources/config/symfony_profiler.yaml index 4deb418d..dcdb6462 100644 --- a/Resources/config/symfony_profiler.yaml +++ b/Resources/config/symfony_profiler.yaml @@ -8,9 +8,10 @@ services: Translation\Bundle\Controller\SymfonyProfilerController: autowire: true public: true - tags: ['container.service_subscriber'] arguments: - '@Translation\Bundle\Service\StorageService' + - '@twig' + - '@router' - '%php_translation.toolbar.allow_edit%' calls: - setProfiler: ['@?profiler'] diff --git a/Resources/config/webui.yaml b/Resources/config/webui.yaml index a1c65caa..654dc11e 100644 --- a/Resources/config/webui.yaml +++ b/Resources/config/webui.yaml @@ -9,6 +9,7 @@ services: - '@Translation\Bundle\Catalogue\CatalogueManager' - '@Translation\Bundle\Service\StorageManager' - '@Symfony\Component\Validator\Validator\ValidatorInterface' + - '@twig' - '%php_translation.locales%' - '%php_translation.webui.enabled%' - '%php_translation.webui.allow_create%' From d47279e2909bfcca8a7801e105e2da8a19b2367d Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Mon, 24 Jan 2022 13:40:51 +0200 Subject: [PATCH 193/234] Use autowiring for SymfonyProfilerController (#461) * Use autowiring for SymfonyProfilerController * Move autowire to the _defaults and remove another arg --- Resources/config/symfony_profiler.yaml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Resources/config/symfony_profiler.yaml b/Resources/config/symfony_profiler.yaml index dcdb6462..7c32adab 100644 --- a/Resources/config/symfony_profiler.yaml +++ b/Resources/config/symfony_profiler.yaml @@ -1,17 +1,15 @@ services: + _defaults: + autowire: true + bind: + $isToolbarAllowEdit: '%php_translation.toolbar.allow_edit%' + php_translation.data_collector: class: Symfony\Component\Translation\DataCollector\TranslationDataCollector - arguments: [ '@translator.data_collector' ] tags: - { name: 'data_collector', template: "@Translation/SymfonyProfiler/translation.html.twig", id: "translation", priority: 200 } Translation\Bundle\Controller\SymfonyProfilerController: - autowire: true public: true - arguments: - - '@Translation\Bundle\Service\StorageService' - - '@twig' - - '@router' - - '%php_translation.toolbar.allow_edit%' calls: - setProfiler: ['@?profiler'] From 004c8f40b24b97d94da7d3d5b8ba305da57b1f0a Mon Sep 17 00:00:00 2001 From: Alexis Urien Date: Mon, 24 Jan 2022 18:23:30 +0100 Subject: [PATCH 194/234] Fix Cannot autowire service "php_translation.data_collector" error introduced in d47279e (#463) Co-authored-by: Alexis Urien --- Resources/config/symfony_profiler.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/config/symfony_profiler.yaml b/Resources/config/symfony_profiler.yaml index 7c32adab..62d8d1b3 100644 --- a/Resources/config/symfony_profiler.yaml +++ b/Resources/config/symfony_profiler.yaml @@ -6,6 +6,7 @@ services: php_translation.data_collector: class: Symfony\Component\Translation\DataCollector\TranslationDataCollector + arguments: [ '@translator.data_collector' ] tags: - { name: 'data_collector', template: "@Translation/SymfonyProfiler/translation.html.twig", id: "translation", priority: 200 } From 75047fd04f6f951dbf223c221ee974856bd59c7b Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Tue, 25 Jan 2022 10:10:49 +0200 Subject: [PATCH 195/234] Fix yaml identation to 4 spaces (#462) --- Resources/config/auto_add.yaml | 8 +-- Resources/config/auto_translation.yaml | 46 ++++++------- Resources/config/console.yaml | 42 ++++++------ Resources/config/edit_in_place.yaml | 66 +++++++++---------- .../config/routing_symfony_profiler.yaml | 25 ++++--- Resources/config/routing_webui.yaml | 1 - Resources/config/services.yaml | 66 +++++++++---------- Resources/config/symfony_profiler.yaml | 26 ++++---- 8 files changed, 139 insertions(+), 141 deletions(-) diff --git a/Resources/config/auto_add.yaml b/Resources/config/auto_add.yaml index 56a5430c..b69ddc17 100644 --- a/Resources/config/auto_add.yaml +++ b/Resources/config/auto_add.yaml @@ -1,5 +1,5 @@ services: - Translation\Bundle\EventListener\AutoAddMissingTranslations: - arguments: [ ~, '@?translator.data_collector' ] - tags: - - { name: kernel.event_listener, event: kernel.terminate, method: onTerminate, priority: 10 } + Translation\Bundle\EventListener\AutoAddMissingTranslations: + arguments: [ ~, '@?translator.data_collector' ] + tags: + - { name: kernel.event_listener, event: kernel.terminate, method: onTerminate, priority: 10 } diff --git a/Resources/config/auto_translation.yaml b/Resources/config/auto_translation.yaml index 683fac7a..aed67648 100644 --- a/Resources/config/auto_translation.yaml +++ b/Resources/config/auto_translation.yaml @@ -1,28 +1,28 @@ services: - Translation\Bundle\Translator\FallbackTranslator: - public: false - decorates: 'translator' - decoration_priority: 10 - arguments: - - '%php_translation.default_locale%' - - '@Translation\Bundle\Translator\FallbackTranslator.inner' - - '@php_translation.translator_service.external_translator' + Translation\Bundle\Translator\FallbackTranslator: + public: false + decorates: 'translator' + decoration_priority: 10 + arguments: + - '%php_translation.default_locale%' + - '@Translation\Bundle\Translator\FallbackTranslator.inner' + - '@php_translation.translator_service.external_translator' - php_translation.translator_service.external_translator: - class: Translation\Translator\Translator - arguments: [] - calls: - - [ 'setLogger', ['@?logger']] + php_translation.translator_service.external_translator: + class: Translation\Translator\Translator + arguments: [] + calls: + - [ 'setLogger', ['@?logger']] - # ----- Services ------ - php_translation.translator_service.google: - class: Translation\Translator\Service\GoogleTranslator - arguments: ['%php_translation.translator_service.api_key%'] + # ----- Services ------ + php_translation.translator_service.google: + class: Translation\Translator\Service\GoogleTranslator + arguments: ['%php_translation.translator_service.api_key%'] - php_translation.translator_service.yandex: - class: Translation\Translator\Service\YandexTranslator - arguments: ['%php_translation.translator_service.api_key%'] + php_translation.translator_service.yandex: + class: Translation\Translator\Service\YandexTranslator + arguments: ['%php_translation.translator_service.api_key%'] - php_translation.translator_service.bing: - class: Translation\Translator\Service\BingTranslator - arguments: ['%php_translation.translator_service.api_key%'] + php_translation.translator_service.bing: + class: Translation\Translator\Service\BingTranslator + arguments: ['%php_translation.translator_service.api_key%'] diff --git a/Resources/config/console.yaml b/Resources/config/console.yaml index 2a003472..9fbe15b3 100644 --- a/Resources/config/console.yaml +++ b/Resources/config/console.yaml @@ -12,56 +12,56 @@ services: Translation\Bundle\Command\DeleteEmptyCommand: public: true arguments: - - '@Translation\Bundle\Service\StorageManager' - - '@Translation\Bundle\Service\ConfigurationManager' - - '@Translation\Bundle\Catalogue\CatalogueManager' - - '@Translation\Bundle\Catalogue\CatalogueFetcher' + - '@Translation\Bundle\Service\StorageManager' + - '@Translation\Bundle\Service\ConfigurationManager' + - '@Translation\Bundle\Catalogue\CatalogueManager' + - '@Translation\Bundle\Catalogue\CatalogueFetcher' tags: - { name: console.command, command: translation:delete-empty } Translation\Bundle\Command\DeleteObsoleteCommand: public: true arguments: - - '@Translation\Bundle\Service\StorageManager' - - '@Translation\Bundle\Service\ConfigurationManager' - - '@Translation\Bundle\Catalogue\CatalogueManager' - - '@Translation\Bundle\Catalogue\CatalogueFetcher' + - '@Translation\Bundle\Service\StorageManager' + - '@Translation\Bundle\Service\ConfigurationManager' + - '@Translation\Bundle\Catalogue\CatalogueManager' + - '@Translation\Bundle\Catalogue\CatalogueFetcher' tags: - { name: console.command, command: translation:delete-obsolete } Translation\Bundle\Command\DownloadCommand: public: true arguments: - - '@Translation\Bundle\Service\StorageManager' - - '@Translation\Bundle\Service\ConfigurationManager' - - '@Translation\Bundle\Service\CacheClearer' - - '@Translation\Bundle\Catalogue\CatalogueWriter' + - '@Translation\Bundle\Service\StorageManager' + - '@Translation\Bundle\Service\ConfigurationManager' + - '@Translation\Bundle\Service\CacheClearer' + - '@Translation\Bundle\Catalogue\CatalogueWriter' tags: - { name: console.command, command: translation:download } Translation\Bundle\Command\ExtractCommand: public: true arguments: - - '@Translation\Bundle\Catalogue\CatalogueFetcher' - - '@Translation\Bundle\Catalogue\CatalogueWriter' - - '@Translation\Bundle\Catalogue\CatalogueCounter' - - '@Translation\Bundle\Service\Importer' - - '@Translation\Bundle\Service\ConfigurationManager' + - '@Translation\Bundle\Catalogue\CatalogueFetcher' + - '@Translation\Bundle\Catalogue\CatalogueWriter' + - '@Translation\Bundle\Catalogue\CatalogueCounter' + - '@Translation\Bundle\Service\Importer' + - '@Translation\Bundle\Service\ConfigurationManager' tags: - { name: console.command, command: translation:extract } Translation\Bundle\Command\StatusCommand: public: true arguments: - - '@Translation\Bundle\Catalogue\CatalogueCounter' - - '@Translation\Bundle\Service\ConfigurationManager' - - '@Translation\Bundle\Catalogue\CatalogueFetcher' + - '@Translation\Bundle\Catalogue\CatalogueCounter' + - '@Translation\Bundle\Service\ConfigurationManager' + - '@Translation\Bundle\Catalogue\CatalogueFetcher' tags: - { name: console.command, command: translation:status } Translation\Bundle\Command\SyncCommand: public: true arguments: - - '@Translation\Bundle\Service\StorageManager' + - '@Translation\Bundle\Service\StorageManager' tags: - { name: console.command, command: translation:sync } diff --git a/Resources/config/edit_in_place.yaml b/Resources/config/edit_in_place.yaml index 47f4220e..c6ac41cd 100644 --- a/Resources/config/edit_in_place.yaml +++ b/Resources/config/edit_in_place.yaml @@ -1,38 +1,38 @@ services: - Translation\Bundle\Controller\EditInPlaceController: - autowire: true - public: true - tags: ['controller.service_arguments'] - arguments: - - '@Translation\Bundle\Service\StorageManager' - - '@Translation\Bundle\Service\CacheClearer' - - '@Symfony\Component\Validator\Validator\ValidatorInterface' + Translation\Bundle\Controller\EditInPlaceController: + autowire: true + public: true + tags: ['controller.service_arguments'] + arguments: + - '@Translation\Bundle\Service\StorageManager' + - '@Translation\Bundle\Service\CacheClearer' + - '@Symfony\Component\Validator\Validator\ValidatorInterface' - Translation\Bundle\EventListener\EditInPlaceResponseListener: - tags: - - { name: 'kernel.event_listener', event: 'kernel.response', method: 'onKernelResponse' } - arguments: - - ~ - - '@router' - - '@assets.packages' - - ~ - - ~ + Translation\Bundle\EventListener\EditInPlaceResponseListener: + tags: + - { name: 'kernel.event_listener', event: 'kernel.response', method: 'onKernelResponse' } + arguments: + - ~ + - '@router' + - '@assets.packages' + - ~ + - ~ - Translation\Bundle\EditInPlace\Activator: - arguments: ['@session'] - public: true + Translation\Bundle\EditInPlace\Activator: + arguments: ['@session'] + public: true - Translation\Bundle\Translator\EditInPlaceTranslator: - arguments: - - '@translator' - - ~ - - '@request_stack' + Translation\Bundle\Translator\EditInPlaceTranslator: + arguments: + - '@translator' + - ~ + - '@request_stack' - Translation\Bundle\Twig\EditInPlaceExtension: - public: false - arguments: - - '@twig.extension.trans' - - '@request_stack' - - ~ - tags: - - { name: 'twig.extension' } + Translation\Bundle\Twig\EditInPlaceExtension: + public: false + arguments: + - '@twig.extension.trans' + - '@request_stack' + - ~ + tags: + - { name: 'twig.extension' } diff --git a/Resources/config/routing_symfony_profiler.yaml b/Resources/config/routing_symfony_profiler.yaml index da5c999d..73f78bbf 100644 --- a/Resources/config/routing_symfony_profiler.yaml +++ b/Resources/config/routing_symfony_profiler.yaml @@ -1,20 +1,19 @@ - php_translation_profiler_translation_edit: - path: /{token}/translation/edit - methods: ["GET", "POST"] - defaults: { _controller: Translation\Bundle\Controller\SymfonyProfilerController::editAction } + path: /{token}/translation/edit + methods: ["GET", "POST"] + defaults: { _controller: Translation\Bundle\Controller\SymfonyProfilerController::editAction } php_translation_profiler_translation_sync: - path: /{token}/translation/sync - methods: ["POST"] - defaults: { _controller: Translation\Bundle\Controller\SymfonyProfilerController::syncAction } + path: /{token}/translation/sync + methods: ["POST"] + defaults: { _controller: Translation\Bundle\Controller\SymfonyProfilerController::syncAction } php_translation_profiler_translation_sync_all: - path: /{token}/translation/sync_all - methods: ["POST"] - defaults: { _controller: Translation\Bundle\Controller\SymfonyProfilerController::syncAllAction } + path: /{token}/translation/sync_all + methods: ["POST"] + defaults: { _controller: Translation\Bundle\Controller\SymfonyProfilerController::syncAllAction } php_translation_profiler_translation_create_assets: - path: /{token}/translation/create_assets - methods: ["POST"] - defaults: { _controller: Translation\Bundle\Controller\SymfonyProfilerController::createAssetsAction } + path: /{token}/translation/create_assets + methods: ["POST"] + defaults: { _controller: Translation\Bundle\Controller\SymfonyProfilerController::createAssetsAction } diff --git a/Resources/config/routing_webui.yaml b/Resources/config/routing_webui.yaml index 780f5f96..1d2212de 100644 --- a/Resources/config/routing_webui.yaml +++ b/Resources/config/routing_webui.yaml @@ -1,4 +1,3 @@ - translation_index: path: /_trans/{configName} methods: [GET] diff --git a/Resources/config/services.yaml b/Resources/config/services.yaml index 646e1acb..70a0f0ba 100644 --- a/Resources/config/services.yaml +++ b/Resources/config/services.yaml @@ -1,45 +1,45 @@ services: - Translation\Bundle\Catalogue\CatalogueFetcher: - public: true - arguments: ['@translation.reader'] + Translation\Bundle\Catalogue\CatalogueFetcher: + public: true + arguments: ['@translation.reader'] - Translation\Bundle\Catalogue\CatalogueWriter: - public: true - arguments: ['@translation.writer', '%php_translation.default_locale%'] + Translation\Bundle\Catalogue\CatalogueWriter: + public: true + arguments: ['@translation.writer', '%php_translation.default_locale%'] - php_translation.storage.abstract: - class: Translation\Bundle\Service\StorageService - abstract: true - arguments: ['@Translation\Bundle\Catalogue\CatalogueFetcher', ~] + php_translation.storage.abstract: + class: Translation\Bundle\Service\StorageService + abstract: true + arguments: ['@Translation\Bundle\Catalogue\CatalogueFetcher', ~] - Translation\Bundle\Catalogue\CatalogueManager: - public: true + Translation\Bundle\Catalogue\CatalogueManager: + public: true - Translation\Extractor\Extractor: ~ + Translation\Extractor\Extractor: ~ - Translation\Bundle\Service\StorageManager: - public: true + Translation\Bundle\Service\StorageManager: + public: true - Translation\Bundle\Service\ConfigurationManager: - public: true + Translation\Bundle\Service\ConfigurationManager: + public: true - Translation\Bundle\Service\Importer: - public: true - arguments: ['@Translation\Extractor\Extractor', '@twig', '%php_translation.default_locale%'] + Translation\Bundle\Service\Importer: + public: true + arguments: ['@Translation\Extractor\Extractor', '@twig', '%php_translation.default_locale%'] - Translation\Bundle\Service\CacheClearer: - public: true - arguments: ['%kernel.cache_dir%', '@translator', '@filesystem'] + Translation\Bundle\Service\CacheClearer: + public: true + arguments: ['%kernel.cache_dir%', '@translator', '@filesystem'] - php_translation.local_file_storage.abstract: - class: Translation\SymfonyStorage\FileStorage - abstract: true - arguments: ['@translation.writer', '@translation.reader', ~, []] + php_translation.local_file_storage.abstract: + class: Translation\SymfonyStorage\FileStorage + abstract: true + arguments: ['@translation.writer', '@translation.reader', ~, []] - Translation\Bundle\Catalogue\CatalogueCounter: - public: true + Translation\Bundle\Catalogue\CatalogueCounter: + public: true - Translation\Bundle\Twig\TranslationExtension: - arguments: ['@translator', '%kernel.debug%'] - tags: - - { name: twig.extension } + Translation\Bundle\Twig\TranslationExtension: + arguments: ['@translator', '%kernel.debug%'] + tags: + - { name: twig.extension } diff --git a/Resources/config/symfony_profiler.yaml b/Resources/config/symfony_profiler.yaml index 62d8d1b3..402a90bf 100644 --- a/Resources/config/symfony_profiler.yaml +++ b/Resources/config/symfony_profiler.yaml @@ -1,16 +1,16 @@ services: - _defaults: - autowire: true - bind: - $isToolbarAllowEdit: '%php_translation.toolbar.allow_edit%' + _defaults: + autowire: true + bind: + $isToolbarAllowEdit: '%php_translation.toolbar.allow_edit%' - php_translation.data_collector: - class: Symfony\Component\Translation\DataCollector\TranslationDataCollector - arguments: [ '@translator.data_collector' ] - tags: - - { name: 'data_collector', template: "@Translation/SymfonyProfiler/translation.html.twig", id: "translation", priority: 200 } + php_translation.data_collector: + class: Symfony\Component\Translation\DataCollector\TranslationDataCollector + arguments: [ '@translator.data_collector' ] + tags: + - { name: 'data_collector', template: "@Translation/SymfonyProfiler/translation.html.twig", id: "translation", priority: 200 } - Translation\Bundle\Controller\SymfonyProfilerController: - public: true - calls: - - setProfiler: ['@?profiler'] + Translation\Bundle\Controller\SymfonyProfilerController: + public: true + calls: + - setProfiler: ['@?profiler'] From 2c3a0c28f81bbee2c69a4b51f7cd4f3b1fefdd6f Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Fri, 28 Jan 2022 11:13:33 +0200 Subject: [PATCH 196/234] Add testing on the newest PHP 8.1 to the CI config (#465) --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e54b88d..10f239ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '7.2', '7.3', '7.4', '8.0' ] + php: [ '7.2', '7.3', '7.4', '8.0', '8.1' ] strategy: [ 'highest' ] sf_version: [''] include: @@ -43,4 +43,3 @@ jobs: - name: Run tests run: ./vendor/bin/simple-phpunit - From b946f06b2e93bddd4a67984c3617c187411c7053 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Fri, 28 Jan 2022 12:32:09 +0200 Subject: [PATCH 197/234] Allow Symfony 6 (#464) * Allow Symfony 6 * Allow 6.0 for missed symfony/phpunit-bridge too --- composer.json | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index 5d6a2f59..80eaee20 100644 --- a/composer.json +++ b/composer.json @@ -11,30 +11,30 @@ ], "require": { "php": "^7.2 || ^8.0", - "symfony/framework-bundle": "^4.4.20 || ^5.2.5", - "symfony/validator": "^4.4.20 || ^5.2.5", - "symfony/translation": "^4.4.20 || ^5.2.5", - "symfony/twig-bundle": "^4.4.20 || ^5.2.4", - "symfony/finder": "^4.4.20 || ^5.2.4", - "symfony/intl": "^4.4.20 || ^5.2.4", + "symfony/framework-bundle": "^4.4.20 || ^5.2.5 || ^6.0", + "symfony/validator": "^4.4.20 || ^5.2.5 || ^6.0", + "symfony/translation": "^4.4.20 || ^5.2.5 || ^6.0", + "symfony/twig-bundle": "^4.4.20 || ^5.2.4 || ^6.0", + "symfony/finder": "^4.4.20 || ^5.2.4 || ^6.0", + "symfony/intl": "^4.4.20 || ^5.2.4 || ^6.0", "php-translation/symfony-storage": "^2.1", "php-translation/extractor": "^2.0", "nyholm/nsa": "^1.1", "twig/twig": "^2.14.4 || ^3.3", - "symfony/asset": "^4.4.20 || ^5.2.4" + "symfony/asset": "^4.4.20 || ^5.2.4 || ^6.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.2", + "symfony/phpunit-bridge": "^5.2 || ^6.0", "bamarni/composer-bin-plugin": "^1.3", "php-translation/translator": "^1.0", "php-http/curl-client": "^1.7 || ^2.0", "php-http/message": "^1.11", "php-http/message-factory": "^1.0.2", - "symfony/console": "^4.4.20 || ^5.2.5", - "symfony/twig-bridge": "^4.4.20 || ^5.2.5", - "symfony/dependency-injection": "^4.4.20 || ^5.2.5", - "symfony/web-profiler-bundle": "^4.4.20 || ^5.2.4", + "symfony/console": "^4.4.20 || ^5.2.5 || ^6.0", + "symfony/twig-bridge": "^4.4.20 || ^5.2.5 || ^6.0", + "symfony/dependency-injection": "^4.4.20 || ^5.2.5 || ^6.0", + "symfony/web-profiler-bundle": "^4.4.20 || ^5.2.4 || ^6.0", "matthiasnoback/symfony-dependency-injection-test": "^4.1", "matthiasnoback/symfony-config-test": "^4.1", "nyholm/psr7": "^1.1", @@ -59,5 +59,10 @@ "branch-alias": { "dev-master": "0.12-dev" } + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + } } } From ea84216a55b521d79211bdea0e3a5baf7d530d70 Mon Sep 17 00:00:00 2001 From: robbedg Date: Fri, 4 Feb 2022 12:05:44 +0100 Subject: [PATCH 198/234] Fix for deprecated session service in Symfony 6 (#468) * Fix for deprecated session service in Symfony 6 * Keep Backward compatibility with optional service argument * Fix code styling Co-authored-by: Robbe De Geyndt --- EditInPlace/Activator.php | 41 ++++++++++++++++++++++++----- Resources/config/edit_in_place.yaml | 4 ++- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/EditInPlace/Activator.php b/EditInPlace/Activator.php index 1e25b5a2..16c2045c 100644 --- a/EditInPlace/Activator.php +++ b/EditInPlace/Activator.php @@ -12,6 +12,7 @@ namespace Translation\Bundle\EditInPlace; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\Session; /** @@ -24,21 +25,47 @@ final class Activator implements ActivatorInterface const KEY = 'translation_bundle.edit_in_place.enabled'; /** - * @var Session + * @var RequestStack */ - private $session; + private $requestStack; - public function __construct(Session $session) + /** + * @var Session|null + */ + private $session = null; + + public function __construct(RequestStack $requestStack) + { + $this->requestStack = $requestStack; + } + + /** + * Set session if available. + */ + public function setSession(Session $session): void { $this->session = $session; } + /** + * Get session based on availability. + */ + private function getSession(): Session + { + $session = $this->session; + if (null === $session) { + $session = $this->requestStack->getSession(); + } + + return $session; + } + /** * Enable the Edit In Place mode. */ public function activate(): void { - $this->session->set(self::KEY, true); + $this->getSession()->set(self::KEY, true); } /** @@ -46,7 +73,7 @@ public function activate(): void */ public function deactivate(): void { - $this->session->remove(self::KEY); + $this->getSession()->remove(self::KEY); } /** @@ -54,10 +81,10 @@ public function deactivate(): void */ public function checkRequest(Request $request = null): bool { - if (!$this->session->has(self::KEY)) { + if (!$this->getSession()->has(self::KEY)) { return false; } - return $this->session->get(self::KEY, false); + return $this->getSession()->get(self::KEY, false); } } diff --git a/Resources/config/edit_in_place.yaml b/Resources/config/edit_in_place.yaml index c6ac41cd..af60adb3 100644 --- a/Resources/config/edit_in_place.yaml +++ b/Resources/config/edit_in_place.yaml @@ -19,7 +19,9 @@ services: - ~ Translation\Bundle\EditInPlace\Activator: - arguments: ['@session'] + arguments: ['@request_stack'] + calls: + - setSession: ['@?session'] public: true Translation\Bundle\Translator\EditInPlaceTranslator: From 255a9f62fc7a6e7efe0aef5b6770ed0be8886a09 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Mon, 14 Feb 2022 13:39:44 +0200 Subject: [PATCH 199/234] Fix return type and implement getCatalogues() (#470) * Fix return type and implement getCatalogues() * Typecast the result of trans() to string * Fix code styles * Fix static PHPStan * Fix code styles one more time * Fix wrong property name - my bad --- .../Translator/EditInPlaceTranslatorTest.php | 4 +--- Translator/EditInPlaceTranslator.php | 18 +++++++++++++++--- Translator/FallbackTranslator.php | 9 +++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Tests/Unit/Translator/EditInPlaceTranslatorTest.php b/Tests/Unit/Translator/EditInPlaceTranslatorTest.php index 4c77f46a..f5bebfac 100644 --- a/Tests/Unit/Translator/EditInPlaceTranslatorTest.php +++ b/Tests/Unit/Translator/EditInPlaceTranslatorTest.php @@ -69,9 +69,7 @@ public function testDisabled(): void $activator = new FakeActivator(false); $service = new EditInPlaceTranslator($symfonyTranslator, $activator, $requestStack); - $this->assertNull( - $service->trans('key', []) - ); + $this->assertEmpty($service->trans('key', [])); } public function testHtmlTranslation(): void diff --git a/Translator/EditInPlaceTranslator.php b/Translator/EditInPlaceTranslator.php index f2966af3..e795a55a 100644 --- a/Translator/EditInPlaceTranslator.php +++ b/Translator/EditInPlaceTranslator.php @@ -63,22 +63,34 @@ public function __construct($translator, ActivatorInterface $activator, RequestS } /** - * @see Translator::getCatalogue + * @see Translator::getCatalogue() */ public function getCatalogue($locale = null): MessageCatalogueInterface { return $this->translator->getCatalogue($locale); } + /** + * @see Translator::getCatalogues() + */ + public function getCatalogues(): array + { + if (!\method_exists($this->translator, 'getCatalogues')) { + throw new \Exception(\sprintf('%s method is not available! Please, upgrade to Symfony 6 in order to to use it', __METHOD__)); + } + + return $this->translator->getCatalogues(); + } + /** * @see Translator::trans */ - public function trans($id, array $parameters = [], $domain = null, $locale = null): ?string + public function trans($id, array $parameters = [], $domain = null, $locale = null): string { $original = $this->translator->trans($id, $parameters, $domain, $locale); $request = LegacyHelper::getMainRequest($this->requestStack); if (!$this->activator->checkRequest($request)) { - return $original; + return (string) $original; } $plain = $this->translator->trans($id, [], $domain, $locale); diff --git a/Translator/FallbackTranslator.php b/Translator/FallbackTranslator.php index 9b53a74f..b0cddf2b 100644 --- a/Translator/FallbackTranslator.php +++ b/Translator/FallbackTranslator.php @@ -137,6 +137,15 @@ public function getCatalogue($locale = null): MessageCatalogueInterface return $this->symfonyTranslator->getCatalogue($locale); } + public function getCatalogues(): array + { + if (!\method_exists($this->symfonyTranslator, 'getCatalogues')) { + throw new \Exception(\sprintf('%s method is not available! Please, upgrade to Symfony 6 in order to to use it', __METHOD__)); + } + + return $this->symfonyTranslator->getCatalogues(); + } + /** * Passes through all unknown calls onto the translator object. */ From 830cc04ee84736a2f8fd52892b483cf2b02ef0bb Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Mon, 14 Feb 2022 13:52:48 +0200 Subject: [PATCH 200/234] Use a different image for PHP CS Fixer to get latest and upgrade config (#471) * Use a different image for PHP CS Fixer to get latest and upgrade config * Add new cache file from PHP CS Fixer to gitignore * Brake styles * Add --dry-run and --diff * Revert "Brake styles" This reverts commit 5285d20212e57695bff09f9b5e9b824ac6508b4a. * Fix all code styles with the new version of PHP CS Fixer * Fix missing: DomXpath -> DOMXPath * Fix some code styles after master rebase --- .github/workflows/static.yml | 34 ++++--------------- .gitignore | 1 + .php-cs-fixer.dist.php | 21 ++++++++++++ .php_cs | 22 ------------ Catalogue/CatalogueFetcher.php | 2 +- Catalogue/CatalogueManager.php | 4 +-- Catalogue/Operation/ReplaceOperation.php | 4 +-- Command/BundleTrait.php | 8 ++--- Command/CheckMissingCommand.php | 6 ++-- Command/DeleteEmptyCommand.php | 4 +-- Command/DeleteObsoleteCommand.php | 4 +-- Command/DownloadCommand.php | 12 +++---- Command/ExtractCommand.php | 2 +- Command/StatusCommand.php | 2 +- Command/StorageTrait.php | 2 +- Command/SyncCommand.php | 4 +-- Controller/EditInPlaceController.php | 4 +-- Controller/SymfonyProfilerController.php | 10 +++--- Controller/WebUIController.php | 12 +++---- .../CompilerPass/ExternalTranslatorPass.php | 2 +- .../CompilerPass/StoragePass.php | 2 +- DependencyInjection/Configuration.php | 18 +++++----- DependencyInjection/TranslationExtension.php | 4 +-- EditInPlace/Activator.php | 2 +- EventListener/AutoAddMissingTranslations.php | 4 +-- EventListener/EditInPlaceResponseListener.php | 14 ++++---- Legacy/LegacyHelper.php | 2 +- Model/Configuration.php | 2 +- Model/Metadata.php | 4 +-- Model/SfProfilerMessage.php | 2 +- Service/CacheClearer.php | 2 +- Service/ConfigurationManager.php | 4 +-- Service/Importer.php | 4 +-- Service/StorageManager.php | 2 +- Service/StorageService.php | 6 ++-- .../Catalogue/CatalogueFetcherTest.php | 2 +- .../Command/CheckMissingCommandTest.php | 4 +-- .../Functional/Command/ExtractCommandTest.php | 4 +-- .../Functional/Command/StatusCommandTest.php | 2 +- Tests/Functional/Command/SyncCommandTest.php | 2 +- .../Controller/EditInPlaceControllerTest.php | 6 ++-- .../Functional/Controller/EditInPlaceTest.php | 8 ++--- .../Controller/WebUIControllerTest.php | 14 ++++---- Tests/Unit/Catalogue/CatalogueManagerTest.php | 8 ++--- Tests/Unit/Model/ConfigurationTest.php | 2 +- .../Unit/Service/ConfigurationManagerTest.php | 2 +- .../Translator/EditInPlaceTranslatorTest.php | 2 +- .../Translator/FallbackTranslatorTest.php | 2 +- Tests/Unit/Twig/BaseTwigTestCase.php | 4 +-- Tests/Unit/Twig/RemovingNodeVisitorTest.php | 2 +- Translator/EditInPlaceTranslator.php | 12 +++---- Translator/FallbackTranslator.php | 10 +++--- Translator/TranslatorInterface.php | 2 +- Twig/Node/Transchoice.php | 2 +- Twig/TranslationExtension.php | 4 +-- 55 files changed, 152 insertions(+), 174 deletions(-) create mode 100644 .php-cs-fixer.dist.php delete mode 100644 .php_cs diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index a02e633e..b4b5d41e 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -38,35 +38,13 @@ jobs: php-cs-fixer: name: PHP-CS-Fixer - runs-on: Ubuntu-20.04 - + runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Cache PhpCsFixer - uses: actions/cache@v2 - with: - path: .github/.cache/php-cs-fixer/ - key: php-cs-fixer-${{ github.sha }} - restore-keys: php-cs-fixer- - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - coverage: none - - - name: Download dependencies - uses: ramsey/composer-install@v1 - with: - composer-options: --no-interaction --prefer-dist --optimize-autoloader - - - name: Download PHP CS Fixer - run: composer bin php-cs-fixer update --no-interaction --no-progress - - - name: Execute PHP CS Fixer - run: vendor/bin/php-cs-fixer fix --diff-format udiff --dry-run + - uses: actions/checkout@v2 + - name: PHP-CS-Fixer + uses: docker://oskarstark/php-cs-fixer-ga + with: + args: --diff --dry-run psalm: name: Psalm diff --git a/.gitignore b/.gitignore index 4d522194..7c0f5345 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ /phpunit.xml /vendor/ .php_cs.cache +.php-cs-fixer.cache .phpunit.result.cache diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 00000000..9d5191e9 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,21 @@ +exclude(__DIR__.'/vendor') + ->name('*.php') + ->in(__DIR__) +; + +$config = new PhpCsFixer\Config(); +return $config->setRules([ + '@Symfony' => true, + '@Symfony:risky' => true, + 'array_syntax' => ['syntax' => 'short'], + 'native_function_invocation' => true, + 'ordered_imports' => true, + 'declare_strict_types' => false, + 'single_import_per_statement' => false, + ]) + ->setRiskyAllowed(true) + ->setFinder($finder) +; diff --git a/.php_cs b/.php_cs deleted file mode 100644 index f23ada12..00000000 --- a/.php_cs +++ /dev/null @@ -1,22 +0,0 @@ -setRiskyAllowed(true) - ->setRules([ - '@Symfony' => true, - '@Symfony:risky' => true, - 'array_syntax' => array('syntax' => 'short'), - 'native_function_invocation' => true, - 'ordered_imports' => true, - 'declare_strict_types' => false, - 'single_import_per_statement' => false, - ]) - ->setFinder( - PhpCsFixer\Finder::create() - ->in(__DIR__) - ->exclude(__DIR__.'/vendor') - ->name('*.php') - ) -; - -return $config; diff --git a/Catalogue/CatalogueFetcher.php b/Catalogue/CatalogueFetcher.php index 0ee5cf00..504d305e 100644 --- a/Catalogue/CatalogueFetcher.php +++ b/Catalogue/CatalogueFetcher.php @@ -46,7 +46,7 @@ public function getCatalogues(Configuration $config, array $locales = []): array foreach ($locales as $locale) { $currentCatalogue = new MessageCatalogue($locale); foreach ($dirs as $path) { - if (\is_dir($path)) { + if (is_dir($path)) { $this->reader->read($path, $currentCatalogue); } } diff --git a/Catalogue/CatalogueManager.php b/Catalogue/CatalogueManager.php index cd8089d7..5d554c0e 100644 --- a/Catalogue/CatalogueManager.php +++ b/Catalogue/CatalogueManager.php @@ -42,7 +42,7 @@ public function load(array $catalogues): void public function getDomains(): array { /** @var MessageCatalogueInterface $c */ - $c = \reset($this->catalogues); + $c = reset($this->catalogues); return $c->getDomains(); } @@ -107,7 +107,7 @@ public function findMessages(array $config = []): array } } - $messages = \array_filter($messages, static function (CatalogueMessage $m) use ($isNew, $isObsolete, $isApproved, $isEmpty) { + $messages = array_filter($messages, static function (CatalogueMessage $m) use ($isNew, $isObsolete, $isApproved, $isEmpty) { if (null !== $isNew && $m->isNew() !== $isNew) { return false; } diff --git a/Catalogue/Operation/ReplaceOperation.php b/Catalogue/Operation/ReplaceOperation.php index 7b572103..bdf277e2 100644 --- a/Catalogue/Operation/ReplaceOperation.php +++ b/Catalogue/Operation/ReplaceOperation.php @@ -37,7 +37,7 @@ protected function processDomain($domain): void 'new' => [], 'obsolete' => [], ]; - if (\defined(\sprintf('%s::INTL_DOMAIN_SUFFIX', MessageCatalogueInterface::class))) { + if (\defined(sprintf('%s::INTL_DOMAIN_SUFFIX', MessageCatalogueInterface::class))) { $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; } else { $intlDomain = $domain; @@ -160,6 +160,6 @@ public function isArrayAssociative(array $arr): bool return false; } - return \array_keys($arr) !== \range(0, \count($arr) - 1); + return array_keys($arr) !== range(0, \count($arr) - 1); } } diff --git a/Command/BundleTrait.php b/Command/BundleTrait.php index be4cd778..cf8af0c0 100644 --- a/Command/BundleTrait.php +++ b/Command/BundleTrait.php @@ -20,11 +20,11 @@ trait BundleTrait private function configureBundleDirs(InputInterface $input, Configuration $config): void { if ($bundleName = $input->getOption('bundle')) { - if (0 === \strpos($bundleName, '@')) { - if (false === $pos = \strpos($bundleName, '/')) { - $bundleName = \substr($bundleName, 1); + if (0 === strpos($bundleName, '@')) { + if (false === $pos = strpos($bundleName, '/')) { + $bundleName = substr($bundleName, 1); } else { - $bundleName = \substr($bundleName, 1, $pos - 2); + $bundleName = substr($bundleName, 1, $pos - 2); } } diff --git a/Command/CheckMissingCommand.php b/Command/CheckMissingCommand.php index 9618e105..46a09e09 100644 --- a/Command/CheckMissingCommand.php +++ b/Command/CheckMissingCommand.php @@ -91,7 +91,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io = new SymfonyStyle($input, $output); if ($newMessages > 0) { - $io->error(\sprintf('%d new message(s) have been found, run bin/console translation:extract', $newMessages)); + $io->error(sprintf('%d new message(s) have been found, run bin/console translation:extract', $newMessages)); return 1; } @@ -100,7 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($emptyTranslations > 0) { $io->error( - \sprintf('%d messages have empty translations, please provide translations for them', $emptyTranslations) + sprintf('%d messages have empty translations, please provide translations for them', $emptyTranslations) ); return 1; @@ -132,7 +132,7 @@ private function countEmptyTranslations(MessageCatalogueInterface $catalogue): i $total = 0; foreach ($catalogue->getDomains() as $domain) { - $emptyTranslations = \array_filter( + $emptyTranslations = array_filter( $catalogue->all($domain), function (string $message = null): bool { return null === $message || '' === $message; diff --git a/Command/DeleteEmptyCommand.php b/Command/DeleteEmptyCommand.php index 67c4e97a..3c40a997 100644 --- a/Command/DeleteEmptyCommand.php +++ b/Command/DeleteEmptyCommand.php @@ -96,7 +96,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($input->isInteractive()) { $helper = $this->getHelper('question'); - $question = new ConfirmationQuestion(\sprintf('You are about to remove %d translations. Do you wish to continue? (y/N) ', $messageCount), false); + $question = new ConfirmationQuestion(sprintf('You are about to remove %d translations. Do you wish to continue? (y/N) ', $messageCount), false); if (!$helper->ask($input, $output, $question)) { return 0; } @@ -109,7 +109,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int foreach ($messages as $message) { $storage->delete($message->getLocale(), $message->getDomain(), $message->getKey()); if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) { - $output->writeln(\sprintf( + $output->writeln(sprintf( 'Deleted empty message "%s" from domain "%s" and locale "%s"', $message->getKey(), $message->getDomain(), diff --git a/Command/DeleteObsoleteCommand.php b/Command/DeleteObsoleteCommand.php index 67a9dfda..1132559c 100644 --- a/Command/DeleteObsoleteCommand.php +++ b/Command/DeleteObsoleteCommand.php @@ -97,7 +97,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($input->isInteractive()) { $helper = $this->getHelper('question'); - $question = new ConfirmationQuestion(\sprintf('You are about to remove %d translations. Do you wish to continue? (y/N) ', $messageCount), false); + $question = new ConfirmationQuestion(sprintf('You are about to remove %d translations. Do you wish to continue? (y/N) ', $messageCount), false); if (!$helper->ask($input, $output, $question)) { return 0; } @@ -110,7 +110,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int foreach ($messages as $message) { $storage->delete($message->getLocale(), $message->getDomain(), $message->getKey()); if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) { - $output->writeln(\sprintf( + $output->writeln(sprintf( 'Deleted obsolete message "%s" from domain "%s" and locale "%s"', $message->getKey(), $message->getDomain(), diff --git a/Command/DownloadCommand.php b/Command/DownloadCommand.php index a64999d4..d877afa8 100644 --- a/Command/DownloadCommand.php +++ b/Command/DownloadCommand.php @@ -75,7 +75,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $message = 'The --cache option is deprecated as it\'s now the default behaviour of this command.'; $io->note($message); - @\trigger_error($message, \E_USER_DEPRECATED); + @trigger_error($message, \E_USER_DEPRECATED); } $configName = $input->getArgument('configuration'); @@ -117,19 +117,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function hashDirectory(string $directory) { - if (!\is_dir($directory)) { + if (!is_dir($directory)) { return false; } $finder = new Finder(); $finder->files()->in($directory)->notName('/~$/')->sortByName(); - $hash = \hash_init('md5'); + $hash = hash_init('md5'); foreach ($finder as $file) { - \hash_update_file($hash, $file->getRealPath()); + hash_update_file($hash, $file->getRealPath()); } - return \hash_final($hash); + return hash_final($hash); } public function cleanParameters(array $raw) @@ -138,7 +138,7 @@ public function cleanParameters(array $raw) foreach ($raw as $string) { // Assert $string looks like "foo:bar" - list($key, $value) = \explode(':', $string, 2); + list($key, $value) = explode(':', $string, 2); $config[$key][] = $value; } diff --git a/Command/ExtractCommand.php b/Command/ExtractCommand.php index 98dba731..065c6e43 100644 --- a/Command/ExtractCommand.php +++ b/Command/ExtractCommand.php @@ -128,7 +128,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** @var Error $error */ foreach ($errors as $error) { $io->error( - \sprintf("%s\nLine: %s\nMessage: %s", $error->getPath(), $error->getLine(), $error->getMessage()) + sprintf("%s\nLine: %s\nMessage: %s", $error->getPath(), $error->getLine(), $error->getMessage()) ); } } diff --git a/Command/StatusCommand.php b/Command/StatusCommand.php index 157cd480..73e21fa4 100644 --- a/Command/StatusCommand.php +++ b/Command/StatusCommand.php @@ -89,7 +89,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($input->getOption('json')) { - $output->writeln(\json_encode($stats)); + $output->writeln(json_encode($stats)); return 0; } diff --git a/Command/StorageTrait.php b/Command/StorageTrait.php index ca6fe2ef..fef48bbf 100644 --- a/Command/StorageTrait.php +++ b/Command/StorageTrait.php @@ -31,7 +31,7 @@ private function getStorage($configName): StorageService if (null === $storage = $this->storageManager->getStorage($configName)) { $availableStorages = $this->storageManager->getNames(); - throw new \InvalidArgumentException(\sprintf('Unknown storage "%s". Available storages are "%s".', $configName, \implode('", "', $availableStorages))); + throw new \InvalidArgumentException(sprintf('Unknown storage "%s". Available storages are "%s".', $configName, implode('", "', $availableStorages))); } return $storage; diff --git a/Command/SyncCommand.php b/Command/SyncCommand.php index 345affb3..859872ff 100644 --- a/Command/SyncCommand.php +++ b/Command/SyncCommand.php @@ -59,7 +59,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int break; default: - $output->writeln(\sprintf('Direction must be either "up" or "down". Not "%s".', $input->getArgument('direction'))); + $output->writeln(sprintf('Direction must be either "up" or "down". Not "%s".', $input->getArgument('direction'))); return 0; } @@ -78,7 +78,7 @@ public function cleanParameters(array $raw) foreach ($raw as $string) { // Assert $string looks like "foo:bar" - list($key, $value) = \explode(':', $string, 2); + list($key, $value) = explode(':', $string, 2); $config[$key][] = $value; } diff --git a/Controller/EditInPlaceController.php b/Controller/EditInPlaceController.php index 32296ce8..66e2ca41 100644 --- a/Controller/EditInPlaceController.php +++ b/Controller/EditInPlaceController.php @@ -66,11 +66,11 @@ public function editAction(Request $request, string $configName, string $locale) private function getMessages(Request $request, string $locale, array $validationGroups = []): array { $json = $request->getContent(); - $data = \json_decode($json, true); + $data = json_decode($json, true); $messages = []; foreach ($data as $key => $value) { - [$domain, $translationKey] = \explode('|', $key); + [$domain, $translationKey] = explode('|', $key); $message = new Message($translationKey, $domain, $locale, $value); diff --git a/Controller/SymfonyProfilerController.php b/Controller/SymfonyProfilerController.php index cae0e5d9..58be70cf 100644 --- a/Controller/SymfonyProfilerController.php +++ b/Controller/SymfonyProfilerController.php @@ -130,7 +130,7 @@ public function createAssetsAction(Request $request, string $token): Response $uploaded[] = $message; } - return new Response(\sprintf('%s new assets created!', \count($uploaded))); + return new Response(sprintf('%s new assets created!', \count($uploaded))); } private function getMessage(Request $request, string $token): SfProfilerMessage @@ -142,7 +142,7 @@ private function getMessage(Request $request, string $token): SfProfilerMessage $collectorMessages = $this->getMessages($token); if (!isset($collectorMessages[$messageId])) { - throw new NotFoundHttpException(\sprintf('No message with key "%s" was found.', $messageId)); + throw new NotFoundHttpException(sprintf('No message with key "%s" was found.', $messageId)); } $message = SfProfilerMessage::create($collectorMessages[$messageId]); @@ -152,7 +152,7 @@ private function getMessage(Request $request, string $token): SfProfilerMessage $message ->setLocale($requestCollector->getLocale()) - ->setTranslation(\sprintf('[%s]', $message->getTranslation())) + ->setTranslation(sprintf('[%s]', $message->getTranslation())) ; } @@ -172,7 +172,7 @@ protected function getSelectedMessages(Request $request, string $token): array return []; } - $toSave = \array_intersect_key($this->getMessages($token), \array_flip($selected)); + $toSave = array_intersect_key($this->getMessages($token), array_flip($selected)); $messages = []; foreach ($toSave as $data) { @@ -195,7 +195,7 @@ private function getMessages(string $token, string $profileName = 'translation') $messages = $dataCollector->getMessages(); - if (\class_exists(Data::class) && $messages instanceof Data) { + if (class_exists(Data::class) && $messages instanceof Data) { return $messages->getValue(true); } diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index 32d3a2cd..2ce6de07 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -95,7 +95,7 @@ public function indexAction(?string $configName = null): Response foreach ($catalogues as $catalogue) { $locale = $catalogue->getLocale(); $domains = $catalogue->all(); - \ksort($domains); + ksort($domains); $catalogueSize[$locale] = 0; foreach ($domains as $domain => $messages) { $count = \count($messages); @@ -138,8 +138,8 @@ public function showAction(string $configName, string $locale, string $domain): /** @var CatalogueMessage[] $messages */ $messages = $this->catalogueManager->getMessages($locale, $domain); - \usort($messages, function (CatalogueMessage $a, CatalogueMessage $b) { - return \strcmp($a->getKey(), $b->getKey()); + usort($messages, function (CatalogueMessage $a, CatalogueMessage $b) { + return strcmp($a->getKey(), $b->getKey()); }); $content = $this->twig->render('@Translation/WebUI/show.html.twig', [ @@ -179,7 +179,7 @@ public function createAction(Request $request, string $configName, string $local try { $storage->create($message); } catch (StorageException $e) { - throw new BadRequestHttpException(\sprintf('Key "%s" does already exist for "%s" on domain "%s".', $message->getKey(), $locale, $domain), $e); + throw new BadRequestHttpException(sprintf('Key "%s" does already exist for "%s" on domain "%s".', $message->getKey(), $locale, $domain), $e); } catch (\Exception $e) { return new Response($e->getMessage(), Response::HTTP_BAD_REQUEST); } @@ -246,7 +246,7 @@ public function deleteAction(Request $request, string $configName, string $local private function getMessageFromRequest(Request $request): Message { $json = $request->getContent(); - $data = \json_decode($json, true); + $data = json_decode($json, true); $message = new Message($data['key']); if (isset($data['message'])) { $message = $message->withTranslation($data['message']); @@ -262,7 +262,7 @@ private function getMessageFromRequest(Request $request): Message */ private function getLocale2LanguageMap(): array { - $names = \class_exists(Locales::class) + $names = class_exists(Locales::class) ? Locales::getNames('en') : Intl::getLocaleBundle()->getLocaleNames('en'); $map = []; diff --git a/DependencyInjection/CompilerPass/ExternalTranslatorPass.php b/DependencyInjection/CompilerPass/ExternalTranslatorPass.php index e078ae8f..fc01c227 100644 --- a/DependencyInjection/CompilerPass/ExternalTranslatorPass.php +++ b/DependencyInjection/CompilerPass/ExternalTranslatorPass.php @@ -38,7 +38,7 @@ public function process(ContainerBuilder $container): void } // Sort by priority - \asort($translators); + asort($translators); $def = $container->getDefinition('php_translation.translator_service.external_translator'); foreach ($translators as $id => $prio) { diff --git a/DependencyInjection/CompilerPass/StoragePass.php b/DependencyInjection/CompilerPass/StoragePass.php index ac448b6d..3e2b6034 100644 --- a/DependencyInjection/CompilerPass/StoragePass.php +++ b/DependencyInjection/CompilerPass/StoragePass.php @@ -51,7 +51,7 @@ public function process(ContainerBuilder $container): void break; default: - throw new \LogicException(\sprintf('The tag "php_translation.storage" must have a "type" of value "local" or "remote". Value "%s" was provided', $tag['type'])); + throw new \LogicException(sprintf('The tag "php_translation.storage" must have a "type" of value "local" or "remote". Value "%s" was provided', $tag['type'])); } } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index da7753b4..518eea73 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -36,7 +36,7 @@ public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('translation'); // Keep compatibility with symfony/config < 4.2 - if (!\method_exists($treeBuilder, 'getRootNode')) { + if (!method_exists($treeBuilder, 'getRootNode')) { $root = $treeBuilder->root('translation'); } else { $root = $treeBuilder->getRootNode(); @@ -109,26 +109,26 @@ private function configsNode(ArrayNodeDefinition $root): void ->prototype('scalar') ->validate() ->always(function ($value) use ($container) { - $value = \str_replace(\DIRECTORY_SEPARATOR, '/', $value); + $value = str_replace(\DIRECTORY_SEPARATOR, '/', $value); if ('@' === $value[0]) { - if (false === $pos = \strpos($value, '/')) { - $bundleName = \substr($value, 1); + if (false === $pos = strpos($value, '/')) { + $bundleName = substr($value, 1); } else { - $bundleName = \substr($value, 1, $pos - 2); + $bundleName = substr($value, 1, $pos - 2); } $bundles = $container->getParameter('kernel.bundles'); if (!isset($bundles[$bundleName])) { - throw new \Exception(\sprintf('The bundle "%s" does not exist. Available bundles: %s', $bundleName, \array_keys($bundles))); + throw new \Exception(sprintf('The bundle "%s" does not exist. Available bundles: %s', $bundleName, array_keys($bundles))); } $ref = new \ReflectionClass($bundles[$bundleName]); - $value = false === $pos ? \dirname($ref->getFileName()) : \dirname($ref->getFileName()).\substr($value, $pos); + $value = false === $pos ? \dirname($ref->getFileName()) : \dirname($ref->getFileName()).substr($value, $pos); } - if (!\is_dir($value)) { - throw new \Exception(\sprintf('The directory "%s" does not exist.', $value)); + if (!is_dir($value)) { + throw new \Exception(sprintf('The directory "%s" does not exist.', $value)); } return $value; diff --git a/DependencyInjection/TranslationExtension.php b/DependencyInjection/TranslationExtension.php index 7fdb0582..24424863 100644 --- a/DependencyInjection/TranslationExtension.php +++ b/DependencyInjection/TranslationExtension.php @@ -174,7 +174,7 @@ private function enableWebUi(ContainerBuilder $container, array $config): void $path = $container->getParameter('kernel.project_dir'); } - $container->setParameter('php_translation.webui.file_base_path', \rtrim($path, '/').'/'); + $container->setParameter('php_translation.webui.file_base_path', rtrim($path, '/').'/'); } /** @@ -185,7 +185,7 @@ private function enableEditInPlace(ContainerBuilder $container, array $config): $name = $config['edit_in_place']['config_name']; if ('default' !== $name && !isset($config['configs'][$name])) { - throw new InvalidArgumentException(\sprintf('There is no config named "%s".', $name)); + throw new InvalidArgumentException(sprintf('There is no config named "%s".', $name)); } $activatorRef = new Reference($config['edit_in_place']['activator']); diff --git a/EditInPlace/Activator.php b/EditInPlace/Activator.php index 16c2045c..3184f6c0 100644 --- a/EditInPlace/Activator.php +++ b/EditInPlace/Activator.php @@ -22,7 +22,7 @@ */ final class Activator implements ActivatorInterface { - const KEY = 'translation_bundle.edit_in_place.enabled'; + public const KEY = 'translation_bundle.edit_in_place.enabled'; /** * @var RequestStack diff --git a/EventListener/AutoAddMissingTranslations.php b/EventListener/AutoAddMissingTranslations.php index b30ab672..5cfcac66 100644 --- a/EventListener/AutoAddMissingTranslations.php +++ b/EventListener/AutoAddMissingTranslations.php @@ -60,6 +60,6 @@ public function onTerminate(TerminateEvent $event): void // PostResponseEvent have been renamed into ResponseEvent in sf 4.3 // @see https://github.com/symfony/symfony/blob/master/UPGRADE-4.3.md#httpkernel // To be removed once sf ^4.3 become the minimum supported version. -if (!\class_exists(TerminateEvent::class) && \class_exists(PostResponseEvent::class)) { - \class_alias(PostResponseEvent::class, TerminateEvent::class); +if (!class_exists(TerminateEvent::class) && class_exists(PostResponseEvent::class)) { + class_alias(PostResponseEvent::class, TerminateEvent::class); } diff --git a/EventListener/EditInPlaceResponseListener.php b/EventListener/EditInPlaceResponseListener.php index e61c1789..9c296e08 100644 --- a/EventListener/EditInPlaceResponseListener.php +++ b/EventListener/EditInPlaceResponseListener.php @@ -24,7 +24,7 @@ */ final class EditInPlaceResponseListener { - const HTML = <<<'HTML' + public const HTML = <<<'HTML' @@ -95,7 +95,7 @@ public function onKernelResponse(ResponseEvent $event): void if (!$this->showUntranslatable) { $replacement = '"$3"'; } - $content = \preg_replace($pattern, $replacement, $content); + $content = preg_replace($pattern, $replacement, $content); // Remove escaped content (e.g. Javascript) $pattern = '@<x-trans.+data-key="([^&]+)".+data-value="([^&]+)".+<\\/x-trans>@mi'; @@ -103,9 +103,9 @@ public function onKernelResponse(ResponseEvent $event): void if (!$this->showUntranslatable) { $replacement = '$2'; } - $content = \preg_replace($pattern, $replacement, $content); + $content = preg_replace($pattern, $replacement, $content); - $html = \sprintf( + $html = sprintf( self::HTML, $this->packages->getUrl('bundles/translation/css/content-tools.min.css'), $this->packages->getUrl('bundles/translation/js/content-tools.min.js'), @@ -116,7 +116,7 @@ public function onKernelResponse(ResponseEvent $event): void 'locale' => $event->getRequest()->getLocale(), ]) ); - $content = \str_replace('', $html."\n".'', $content); + $content = str_replace('', $html."\n".'', $content); $response = $event->getResponse(); @@ -132,6 +132,6 @@ public function onKernelResponse(ResponseEvent $event): void // FilterResponseEvent have been renamed into ResponseEvent in sf 4.3 // @see https://github.com/symfony/symfony/blob/master/UPGRADE-4.3.md#httpkernel // To be removed once sf ^4.3 become the minimum supported version. -if (!\class_exists(ResponseEvent::class) && \class_exists(FilterResponseEvent::class)) { - \class_alias(FilterResponseEvent::class, ResponseEvent::class); +if (!class_exists(ResponseEvent::class) && class_exists(FilterResponseEvent::class)) { + class_alias(FilterResponseEvent::class, ResponseEvent::class); } diff --git a/Legacy/LegacyHelper.php b/Legacy/LegacyHelper.php index c59cba6d..42d15a87 100644 --- a/Legacy/LegacyHelper.php +++ b/Legacy/LegacyHelper.php @@ -24,7 +24,7 @@ class LegacyHelper { public static function getMainRequest(RequestStack $requestStack) { - if (\method_exists($requestStack, 'getMainRequest')) { + if (method_exists($requestStack, 'getMainRequest')) { return $requestStack->getMainRequest(); } diff --git a/Model/Configuration.php b/Model/Configuration.php index 1945c7bd..e4f7bdac 100644 --- a/Model/Configuration.php +++ b/Model/Configuration.php @@ -169,7 +169,7 @@ public function getWhitelistDomains(): array */ public function getPathsToTranslationFiles(): array { - return \array_merge($this->externalTranslationsDirs, [$this->getOutputDir()]); + return array_merge($this->externalTranslationsDirs, [$this->getOutputDir()]); } public function getXliffVersion(): string diff --git a/Model/Metadata.php b/Model/Metadata.php index 194f7141..e5bbf019 100644 --- a/Model/Metadata.php +++ b/Model/Metadata.php @@ -114,7 +114,7 @@ public function getSourceLocations(): array if (!isset($note['content'])) { continue; } - list($path, $line) = \explode(':', $note['content'], 2); + list($path, $line) = explode(':', $note['content'], 2); $sources[] = ['path' => $path, 'line' => $line]; } @@ -155,7 +155,7 @@ public function getAllInCategory(string $category): array } } - \usort($data, static function (array $a, array $b) { + usort($data, static function (array $a, array $b) { return (int) $a['priority'] - (int) $b['priority']; }); diff --git a/Model/SfProfilerMessage.php b/Model/SfProfilerMessage.php index 078a29a2..e5c597ed 100644 --- a/Model/SfProfilerMessage.php +++ b/Model/SfProfilerMessage.php @@ -123,7 +123,7 @@ public function convertToMessage(): MessageInterface if ($this->hasParameters()) { // Reduce to only get one value of each parameter, not all the usages. - $meta['parameters'] = \array_reduce($this->getParameters(), 'array_merge', []); + $meta['parameters'] = array_reduce($this->getParameters(), 'array_merge', []); } if (!empty($this->getCount())) { diff --git a/Service/CacheClearer.php b/Service/CacheClearer.php index 9b5b44ef..869ad637 100644 --- a/Service/CacheClearer.php +++ b/Service/CacheClearer.php @@ -60,7 +60,7 @@ public function __construct(string $kernelCacheDir, $translator, Filesystem $fil */ public function clearAndWarmUp(?string $locale = null): void { - $translationDir = \sprintf('%s/translations', $this->kernelCacheDir); + $translationDir = sprintf('%s/translations', $this->kernelCacheDir); $finder = new Finder(); diff --git a/Service/ConfigurationManager.php b/Service/ConfigurationManager.php index c4b22386..5a689461 100644 --- a/Service/ConfigurationManager.php +++ b/Service/ConfigurationManager.php @@ -50,7 +50,7 @@ public function getConfiguration($name = null): Configuration } } - throw new \InvalidArgumentException(\sprintf('No configuration found for "%s"', $name)); + throw new \InvalidArgumentException(sprintf('No configuration found for "%s"', $name)); } public function getFirstName(): ?string @@ -64,6 +64,6 @@ public function getFirstName(): ?string public function getNames(): array { - return \array_keys($this->configuration); + return array_keys($this->configuration); } } diff --git a/Service/Importer.php b/Service/Importer.php index 3fb8677b..146add31 100644 --- a/Service/Importer.php +++ b/Service/Importer.php @@ -136,7 +136,7 @@ private function convertSourceLocationsToMessages(MessageCatalogue $catalogue, S $trimLength = 1 + \strlen($this->config['project_root']); $meta = $this->getMetadata($catalogue, $key, $domain); - $meta->addCategory('file-source', \sprintf('%s:%s', \substr($sourceLocation->getPath(), $trimLength), $sourceLocation->getLine())); + $meta->addCategory('file-source', sprintf('%s:%s', substr($sourceLocation->getPath(), $trimLength), $sourceLocation->getLine())); if (isset($sourceLocation->getContext()['desc'])) { $meta->addCategory('desc', $sourceLocation->getContext()['desc']); } @@ -180,7 +180,7 @@ private function processConfig(array $config): void 'whitelist_domains' => [], ]; - $config = \array_merge($default, $config); + $config = array_merge($default, $config); if (!empty($config['blacklist_domains']) && !empty($config['whitelist_domains'])) { throw new \InvalidArgumentException('Cannot use "blacklist_domains" and "whitelist_domains" at the same time'); diff --git a/Service/StorageManager.php b/Service/StorageManager.php index 77d6d5f8..b53eba6c 100644 --- a/Service/StorageManager.php +++ b/Service/StorageManager.php @@ -62,6 +62,6 @@ public function getFirstName(): ?string public function getNames(): array { - return \array_keys($this->storages); + return array_keys($this->storages); } } diff --git a/Service/StorageService.php b/Service/StorageService.php index 2442ae3c..3dfc1dd3 100644 --- a/Service/StorageService.php +++ b/Service/StorageService.php @@ -27,9 +27,9 @@ */ final class StorageService implements Storage { - const DIRECTION_UP = 'up'; + public const DIRECTION_UP = 'up'; - const DIRECTION_DOWN = 'down'; + public const DIRECTION_DOWN = 'down'; /** * @var Storage[] @@ -87,7 +87,7 @@ public function sync(string $direction = self::DIRECTION_DOWN, array $importOpti break; default: - throw new LogicException(\sprintf('Direction must be either "up" or "down". Value "%s" was provided', $direction)); + throw new LogicException(sprintf('Direction must be either "up" or "down". Value "%s" was provided', $direction)); } } diff --git a/Tests/Functional/Catalogue/CatalogueFetcherTest.php b/Tests/Functional/Catalogue/CatalogueFetcherTest.php index eb6034c7..2dcaf816 100644 --- a/Tests/Functional/Catalogue/CatalogueFetcherTest.php +++ b/Tests/Functional/Catalogue/CatalogueFetcherTest.php @@ -25,7 +25,7 @@ public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - \file_put_contents( + file_put_contents( __DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' diff --git a/Tests/Functional/Command/CheckMissingCommandTest.php b/Tests/Functional/Command/CheckMissingCommandTest.php index f64dfbc9..d957f8de 100644 --- a/Tests/Functional/Command/CheckMissingCommandTest.php +++ b/Tests/Functional/Command/CheckMissingCommandTest.php @@ -23,7 +23,7 @@ protected function setUp(): void $this->bootKernel(); $this->application = new Application($this->kernel); - \file_put_contents( + file_put_contents( __DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' @@ -93,7 +93,7 @@ public function testReportsEmptyTranslationMessages(): void public function testReportsNoNewTranslationMessages(): void { - \file_put_contents( + file_put_contents( __DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' diff --git a/Tests/Functional/Command/ExtractCommandTest.php b/Tests/Functional/Command/ExtractCommandTest.php index 1556b849..ff7a9915 100644 --- a/Tests/Functional/Command/ExtractCommandTest.php +++ b/Tests/Functional/Command/ExtractCommandTest.php @@ -27,7 +27,7 @@ protected function setUp(): void parent::setUp(); $this->kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yaml'); - \file_put_contents(__DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' + file_put_contents(__DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' @@ -75,7 +75,7 @@ public function testExecute(): void // transchoice tag have been definively removed in sf ^5.0 // Remove this condition & views_with_transchoice + associated config once sf ^5.0 is the minimum supported version. - if (\version_compare(Kernel::VERSION, 5.0, '<')) { + if (version_compare(Kernel::VERSION, 5.0, '<')) { $configuration = 'app_with_transchoice'; } else { $configuration = 'app'; diff --git a/Tests/Functional/Command/StatusCommandTest.php b/Tests/Functional/Command/StatusCommandTest.php index 2633fb8c..680f65f2 100644 --- a/Tests/Functional/Command/StatusCommandTest.php +++ b/Tests/Functional/Command/StatusCommandTest.php @@ -43,7 +43,7 @@ public function testExecute(): void // the output of the command in the console $output = $commandTester->getDisplay(); - $data = \json_decode($output, true); + $data = json_decode($output, true); $this->assertArrayHasKey('en', $data); $this->assertArrayHasKey('messages', $data['en']); diff --git a/Tests/Functional/Command/SyncCommandTest.php b/Tests/Functional/Command/SyncCommandTest.php index d758cbf0..72154d77 100644 --- a/Tests/Functional/Command/SyncCommandTest.php +++ b/Tests/Functional/Command/SyncCommandTest.php @@ -23,7 +23,7 @@ protected function setUp(): void parent::setUp(); $this->kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yaml'); - \file_put_contents(__DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' + file_put_contents(__DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' diff --git a/Tests/Functional/Controller/EditInPlaceControllerTest.php b/Tests/Functional/Controller/EditInPlaceControllerTest.php index c7d9290a..c2aa72c3 100644 --- a/Tests/Functional/Controller/EditInPlaceControllerTest.php +++ b/Tests/Functional/Controller/EditInPlaceControllerTest.php @@ -23,7 +23,7 @@ public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - \file_put_contents(__DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' + file_put_contents(__DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' @@ -54,7 +54,7 @@ protected function setUp(): void public function testEditAction(): void { - $request = Request::create('/admin/_trans_edit_in_place/app/sv', 'POST', [], [], [], [], \json_encode([ + $request = Request::create('/admin/_trans_edit_in_place/app/sv', 'POST', [], [], [], [], json_encode([ 'messages|key0' => 'trans0', 'messages|key1' => 'trans1', ])); @@ -64,7 +64,7 @@ public function testEditAction(): void public function testEditActionError(): void { - $request = Request::create('/admin/_trans_edit_in_place/app/sv', 'POST', [], [], [], [], \json_encode([ + $request = Request::create('/admin/_trans_edit_in_place/app/sv', 'POST', [], [], [], [], json_encode([ 'messages|key0' => 'trans0', 'messages|' => 'trans1', ])); diff --git a/Tests/Functional/Controller/EditInPlaceTest.php b/Tests/Functional/Controller/EditInPlaceTest.php index 924c8bfd..aaec436d 100644 --- a/Tests/Functional/Controller/EditInPlaceTest.php +++ b/Tests/Functional/Controller/EditInPlaceTest.php @@ -34,8 +34,8 @@ public function testActivatedTest(): void self::assertStringContainsString('', $response->getContent()); $dom = new \DOMDocument('1.0', 'utf-8'); - @$dom->loadHTML(\mb_convert_encoding($response->getContent(), 'HTML-ENTITIES', 'UTF-8')); - $xpath = new \DomXpath($dom); + @$dom->loadHTML(mb_convert_encoding($response->getContent(), 'HTML-ENTITIES', 'UTF-8')); + $xpath = new \DOMXPath($dom); // Check number of x-trans tags $xtrans = $xpath->query('//x-trans'); @@ -66,8 +66,8 @@ public function testIfUntranslatableLabelGetsDisabled(): void self::assertStringContainsString('', $response->getContent()); $dom = new \DOMDocument('1.0', 'utf-8'); - @$dom->loadHTML(\mb_convert_encoding($response->getContent(), 'HTML-ENTITIES', 'UTF-8')); - $xpath = new \DomXpath($dom); + @$dom->loadHTML(mb_convert_encoding($response->getContent(), 'HTML-ENTITIES', 'UTF-8')); + $xpath = new \DOMXPath($dom); // Check number of x-trans tags $xtrans = $xpath->query('//x-trans'); diff --git a/Tests/Functional/Controller/WebUIControllerTest.php b/Tests/Functional/Controller/WebUIControllerTest.php index 94838143..d8b31915 100644 --- a/Tests/Functional/Controller/WebUIControllerTest.php +++ b/Tests/Functional/Controller/WebUIControllerTest.php @@ -20,7 +20,7 @@ public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - \file_put_contents(__DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' + file_put_contents(__DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' @@ -69,13 +69,13 @@ public function testShowAction(): void public function testCreateAction(): void { - $request = Request::create('/_trans/app/sv/messages/new', 'POST', [], [], [], [], \json_encode([ + $request = Request::create('/_trans/app/sv/messages/new', 'POST', [], [], [], [], json_encode([ 'key' => 'foo', ])); $response = $this->kernel->handle($request); $this->assertEquals(400, $response->getStatusCode()); - $request = Request::create('/_trans/app/sv/messages/new', 'POST', [], [], [], [], \json_encode([ + $request = Request::create('/_trans/app/sv/messages/new', 'POST', [], [], [], [], json_encode([ 'key' => 'foo', 'message' => 'bar', ])); @@ -85,13 +85,13 @@ public function testCreateAction(): void public function testEditAction(): void { - $request = Request::create('/_trans/app/sv/messages', 'POST', [], [], [], [], \json_encode([ + $request = Request::create('/_trans/app/sv/messages', 'POST', [], [], [], [], json_encode([ 'key' => 'foo', ])); $response = $this->kernel->handle($request); $this->assertEquals(400, $response->getStatusCode()); - $request = Request::create('/_trans/app/sv/messages', 'POST', [], [], [], [], \json_encode([ + $request = Request::create('/_trans/app/sv/messages', 'POST', [], [], [], [], json_encode([ 'key' => 'key1', 'message' => 'bar', ])); @@ -102,13 +102,13 @@ public function testEditAction(): void public function testDeleteAction(): void { // Removing something that does not exists is okey. - $request = Request::create('/_trans/app/sv/messages', 'DELETE', [], [], [], [], \json_encode([ + $request = Request::create('/_trans/app/sv/messages', 'DELETE', [], [], [], [], json_encode([ 'key' => 'empty', ])); $response = $this->kernel->handle($request); $this->assertEquals(200, $response->getStatusCode()); - $request = Request::create('/_trans/app/sv/messages', 'DELETE', [], [], [], [], \json_encode([ + $request = Request::create('/_trans/app/sv/messages', 'DELETE', [], [], [], [], json_encode([ 'key' => 'foo', ])); $response = $this->kernel->handle($request); diff --git a/Tests/Unit/Catalogue/CatalogueManagerTest.php b/Tests/Unit/Catalogue/CatalogueManagerTest.php index bd856fd8..e2de8022 100644 --- a/Tests/Unit/Catalogue/CatalogueManagerTest.php +++ b/Tests/Unit/Catalogue/CatalogueManagerTest.php @@ -60,12 +60,12 @@ public function testFindMessages(): void // Only one approved en message $messages = $manager->findMessages(['locale' => 'en', 'isApproved' => true]); $this->assertCount(1, $messages); - $messages = \array_values($messages); + $messages = array_values($messages); $this->assertEquals('d', $messages[0]->getKey()); $messages = $manager->findMessages(['isApproved' => true]); $this->assertCount(3, $messages); - $keys = \array_map(function (CatalogueMessage $message) { + $keys = array_map(function (CatalogueMessage $message) { return $message->getKey(); }, $messages); $this->assertContains('c', $keys); @@ -74,12 +74,12 @@ public function testFindMessages(): void $messages = $manager->findMessages(['isNew' => true]); $this->assertCount(1, $messages); - $messages = \array_values($messages); + $messages = array_values($messages); $this->assertEquals('a', $messages[0]->getKey()); $messages = $manager->findMessages(['isObsolete' => true]); $this->assertCount(1, $messages); - $messages = \array_values($messages); + $messages = array_values($messages); $this->assertEquals('b', $messages[0]->getKey()); } } diff --git a/Tests/Unit/Model/ConfigurationTest.php b/Tests/Unit/Model/ConfigurationTest.php index 922cb156..7160b491 100644 --- a/Tests/Unit/Model/ConfigurationTest.php +++ b/Tests/Unit/Model/ConfigurationTest.php @@ -24,7 +24,7 @@ public function testAccessors(): void foreach ($key2Function as $key => $value) { $func = $value; if (\is_array($func)) { - $func = \reset($func); + $func = reset($func); } $this->assertEquals($value, $conf->$func()); } diff --git a/Tests/Unit/Service/ConfigurationManagerTest.php b/Tests/Unit/Service/ConfigurationManagerTest.php index d553e98b..0ea8d158 100644 --- a/Tests/Unit/Service/ConfigurationManagerTest.php +++ b/Tests/Unit/Service/ConfigurationManagerTest.php @@ -96,6 +96,6 @@ private function createConfiguration(array $data = []): Configuration { $default = ConfigurationTest::getDefaultData(); - return new Configuration(\array_merge($default, $data)); + return new Configuration(array_merge($default, $data)); } } diff --git a/Tests/Unit/Translator/EditInPlaceTranslatorTest.php b/Tests/Unit/Translator/EditInPlaceTranslatorTest.php index f5bebfac..80e04004 100644 --- a/Tests/Unit/Translator/EditInPlaceTranslatorTest.php +++ b/Tests/Unit/Translator/EditInPlaceTranslatorTest.php @@ -27,7 +27,7 @@ final class EditInPlaceTranslatorTest extends TestCase { public function testWithNotLocaleAwareTranslator() { - if (!\interface_exists(NewTranslatorInterface::class)) { + if (!interface_exists(NewTranslatorInterface::class)) { $this->markTestSkipped('Relevant only when NewTranslatorInterface is available.'); } diff --git a/Tests/Unit/Translator/FallbackTranslatorTest.php b/Tests/Unit/Translator/FallbackTranslatorTest.php index 2620c3a7..bafd6521 100644 --- a/Tests/Unit/Translator/FallbackTranslatorTest.php +++ b/Tests/Unit/Translator/FallbackTranslatorTest.php @@ -26,7 +26,7 @@ final class FallbackTranslatorTest extends TestCase { public function testWithNotLocaleAwareTranslator() { - if (!\interface_exists(NewTranslatorInterface::class)) { + if (!interface_exists(NewTranslatorInterface::class)) { $this->markTestSkipped('Relevant only when NewTranslatorInterface is available.'); } diff --git a/Tests/Unit/Twig/BaseTwigTestCase.php b/Tests/Unit/Twig/BaseTwigTestCase.php index f404a6d0..4467708e 100644 --- a/Tests/Unit/Twig/BaseTwigTestCase.php +++ b/Tests/Unit/Twig/BaseTwigTestCase.php @@ -26,9 +26,9 @@ abstract class BaseTwigTestCase extends TestCase { final protected function parse(string $file, bool $debug = false): string { - $content = \file_get_contents(__DIR__.'/Fixture/'.$file); + $content = file_get_contents(__DIR__.'/Fixture/'.$file); - $loader = \class_exists(ArrayLoader::class) + $loader = class_exists(ArrayLoader::class) ? new ArrayLoader() : new \Twig_Loader_Array([]); $env = new Environment($loader); diff --git a/Tests/Unit/Twig/RemovingNodeVisitorTest.php b/Tests/Unit/Twig/RemovingNodeVisitorTest.php index 818e6515..7c5fc5d7 100644 --- a/Tests/Unit/Twig/RemovingNodeVisitorTest.php +++ b/Tests/Unit/Twig/RemovingNodeVisitorTest.php @@ -22,7 +22,7 @@ public function testRemovalWithSimpleTemplate(): void { // transchoice tag have been definively removed in sf ^5.0 // Remove this condition & *with_transchoice templates once sf ^5.0 is the minimum supported version. - if (\version_compare(Kernel::VERSION, 5.0, '<')) { + if (version_compare(Kernel::VERSION, 5.0, '<')) { $expected = $this->parse('simple_template_compiled_with_transchoice.html.twig'); $actual = $this->parse('simple_template_with_transchoice.html.twig'); } else { diff --git a/Translator/EditInPlaceTranslator.php b/Translator/EditInPlaceTranslator.php index e795a55a..5c2c1dd8 100644 --- a/Translator/EditInPlaceTranslator.php +++ b/Translator/EditInPlaceTranslator.php @@ -75,8 +75,8 @@ public function getCatalogue($locale = null): MessageCatalogueInterface */ public function getCatalogues(): array { - if (!\method_exists($this->translator, 'getCatalogues')) { - throw new \Exception(\sprintf('%s method is not available! Please, upgrade to Symfony 6 in order to to use it', __METHOD__)); + if (!method_exists($this->translator, 'getCatalogues')) { + throw new \Exception(sprintf('%s method is not available! Please, upgrade to Symfony 6 in order to to use it', __METHOD__)); } return $this->translator->getCatalogues(); @@ -103,11 +103,11 @@ public function trans($id, array $parameters = [], $domain = null, $locale = nul } // Render all data in the translation tag required to allow in-line translation - return \sprintf('%s', + return sprintf('%s', $domain, $id, - \htmlspecialchars($original), - \htmlspecialchars($plain), + htmlspecialchars($original), + htmlspecialchars($plain), $domain, $locale, $original @@ -124,7 +124,7 @@ public function transChoice($id, $number, array $parameters = [], $domain = null return $this->translator->transChoice($id, $number, $parameters, $domain, $locale); } - $parameters = \array_merge([ + $parameters = array_merge([ '%count%' => $number, ], $parameters); diff --git a/Translator/FallbackTranslator.php b/Translator/FallbackTranslator.php index b0cddf2b..f49222c2 100644 --- a/Translator/FallbackTranslator.php +++ b/Translator/FallbackTranslator.php @@ -139,8 +139,8 @@ public function getCatalogue($locale = null): MessageCatalogueInterface public function getCatalogues(): array { - if (!\method_exists($this->symfonyTranslator, 'getCatalogues')) { - throw new \Exception(\sprintf('%s method is not available! Please, upgrade to Symfony 6 in order to to use it', __METHOD__)); + if (!method_exists($this->symfonyTranslator, 'getCatalogues')) { + throw new \Exception(sprintf('%s method is not available! Please, upgrade to Symfony 6 in order to to use it', __METHOD__)); } return $this->symfonyTranslator->getCatalogues(); @@ -163,10 +163,10 @@ private function translateWithSubstitutedParameters(string $orgString, string $l // Replace parameters $replacements = []; foreach ($parameters as $placeholder => $nonTranslatableValue) { - $replacements[(string) $nonTranslatableValue] = \sha1($placeholder); + $replacements[(string) $nonTranslatableValue] = sha1($placeholder); } - $replacedString = \str_replace(\array_keys($replacements), \array_values($replacements), $orgString); + $replacedString = str_replace(array_keys($replacements), array_values($replacements), $orgString); $translatedString = $this->externalTranslator->translate($replacedString, $this->defaultLocale, $locale); if (null === $translatedString) { @@ -174,6 +174,6 @@ private function translateWithSubstitutedParameters(string $orgString, string $l return $orgString; } - return \str_replace(\array_values($replacements), \array_keys($replacements), $translatedString); + return str_replace(array_values($replacements), array_keys($replacements), $translatedString); } } diff --git a/Translator/TranslatorInterface.php b/Translator/TranslatorInterface.php index e9bbdfd7..5b98dedb 100644 --- a/Translator/TranslatorInterface.php +++ b/Translator/TranslatorInterface.php @@ -24,7 +24,7 @@ * When sf 3.4 won't be supported anymore, this interface will become useless. */ -if (\interface_exists(NewTranslatorInterface::class)) { +if (interface_exists(NewTranslatorInterface::class)) { interface TranslatorInterface extends NewTranslatorInterface, LocaleAwareInterface, TranslatorBagInterface { } diff --git a/Twig/Node/Transchoice.php b/Twig/Node/Transchoice.php index d1e67753..1212a94d 100644 --- a/Twig/Node/Transchoice.php +++ b/Twig/Node/Transchoice.php @@ -25,7 +25,7 @@ public function __construct(ArrayExpression $arguments, $lineno) public function compile(Compiler $compiler): void { $compiler->raw( - \sprintf( + sprintf( '$this->env->getExtension(\'%s\')->%s(', 'Translation\Bundle\Twig\TranslationExtension', 'transchoiceWithDefault' diff --git a/Twig/TranslationExtension.php b/Twig/TranslationExtension.php index 0cfc97ac..cf039b13 100644 --- a/Twig/TranslationExtension.php +++ b/Twig/TranslationExtension.php @@ -78,10 +78,10 @@ public function transchoiceWithDefault(string $message, string $defaultMessage, } if (false === $this->translator->getCatalogue($locale)->defines($message, $domain)) { - return $this->translator->transChoice($defaultMessage, $count, \array_merge(['%count%' => $count], $arguments), $domain, $locale); + return $this->translator->transChoice($defaultMessage, $count, array_merge(['%count%' => $count], $arguments), $domain, $locale); } - return $this->translator->transChoice($message, $count, \array_merge(['%count%' => $count], $arguments), $domain, $locale); + return $this->translator->transChoice($message, $count, array_merge(['%count%' => $count], $arguments), $domain, $locale); } /** From 02bcf0e0699b4ae5dcf34ebbf1be299fff06b4f5 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Mon, 14 Feb 2022 13:58:15 +0200 Subject: [PATCH 201/234] Prepare release v0.12.5 (#469) * Prepare release v0.12.5 * Add new PR that was just merged * Mention another merged PR about new PHP CS Fixer image --- Changelog.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Changelog.md b/Changelog.md index 17cf2422..f624b10c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,30 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.12.5 + +### Added + +* Add testing on the newest PHP 8.1 to the CI config by @bocharsky-bw in https://github.com/php-translation/symfony-bundle/pull/465 +* Allow Symfony 6 by @bocharsky-bw in https://github.com/php-translation/symfony-bundle/pull/464 + +### Removed + +* refac: remove AbstractController from EditInPlaceController by @gimler in https://github.com/php-translation/symfony-bundle/pull/459 +* Stop extending AbstractController to fix some deprecations by @bocharsky-bw in https://github.com/php-translation/symfony-bundle/pull/460 + +### Changed + +* Use autowiring for SymfonyProfilerController by @bocharsky-bw in https://github.com/php-translation/symfony-bundle/pull/461 +* Use a different image for PHP CS Fixer to get latest and upgrade config #471 + +### Fixed + +* Fix Cannot autowire service "php_translation.data_collector" error in… by @axi in https://github.com/php-translation/symfony-bundle/pull/463 +* Fix yaml indentation to 4 spaces by @bocharsky-bw in https://github.com/php-translation/symfony-bundle/pull/462 +* Fix for deprecated session service in Symfony 6 by @robbedg in https://github.com/php-translation/symfony-bundle/pull/468 +* Fix return type and implement getCatalogues() https://github.com/php-translation/symfony-bundle/pull/470 + ## 0.12.4 ### Fixed From ccfd60e8f8e03cc51e3726df9f36eb847ee5a199 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Tue, 15 Feb 2022 20:20:45 +0200 Subject: [PATCH 202/234] Some minor tweaks in README (#472) --- Readme.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Readme.md b/Readme.md index 88dfc44f..ca4190df 100644 --- a/Readme.md +++ b/Readme.md @@ -9,7 +9,7 @@ Install this bundle via Composer: -``` bash +```bash $ composer require php-translation/symfony-bundle ``` @@ -58,8 +58,8 @@ translation: ```yaml # config/routes/dev/php_translation.yaml _translation_webui: - resource: "@TranslationBundle/Resources/config/routing_webui.yaml" - prefix: /admin + resource: '@TranslationBundle/Resources/config/routing_webui.yaml' + prefix: /admin _translation_profiler: resource: '@TranslationBundle/Resources/config/routing_symfony_profiler.yaml' @@ -69,12 +69,12 @@ _translation_profiler: # config/routes/php_translation.yaml _translation_edit_in_place: resource: '@TranslationBundle/Resources/config/routing_edit_in_place.yaml' - prefix: /admin + prefix: /admin ``` ## Documentation -Read the full documentation at [http://php-translation.readthedocs.io](https://php-translation.readthedocs.io/en/latest/). +Read the full documentation at [https://php-translation.readthedocs.io](https://php-translation.readthedocs.io/en/latest/). [symfony_flex]: https://github.com/symfony/flex From 1c0519ef8f3224cdf168acd8c8707a0705156629 Mon Sep 17 00:00:00 2001 From: Basil Baumgartner <32196755+seizan8@users.noreply.github.com> Date: Mon, 4 Apr 2022 14:26:18 +0200 Subject: [PATCH 203/234] fix: used finder->exclude instead of notPath for excludedDirs (#474) * fix: used finder->exclude instead of notPath for excludedDirs * docu: updated php comment in SymfonyProfilerController Co-authored-by: Basil Baumgartner --- Command/ExtractCommand.php | 2 +- Controller/SymfonyProfilerController.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Command/ExtractCommand.php b/Command/ExtractCommand.php index 065c6e43..7dd27bfd 100644 --- a/Command/ExtractCommand.php +++ b/Command/ExtractCommand.php @@ -143,7 +143,7 @@ private function getConfiguredFinder(Configuration $config): Finder $finder->in($config->getDirs()); foreach ($config->getExcludedDirs() as $exclude) { - $finder->notPath($exclude); + $finder->exclude($exclude); } foreach ($config->getExcludedNames() as $exclude) { diff --git a/Controller/SymfonyProfilerController.php b/Controller/SymfonyProfilerController.php index 58be70cf..d36e54e7 100644 --- a/Controller/SymfonyProfilerController.php +++ b/Controller/SymfonyProfilerController.php @@ -71,7 +71,7 @@ public function editAction(Request $request, string $token): Response return new Response($content); } - //Assert: This is a POST request + // Assert: This is a POST request $message->setTranslation((string) $request->request->get('translation')); $this->storage->update($message->convertToMessage()); From 1a83e47666ebebc27bbf4e57d91c7f0064056a05 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Wed, 6 Apr 2022 14:17:06 +0300 Subject: [PATCH 204/234] Prepare 0.12.6 (#475) --- Changelog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Changelog.md b/Changelog.md index f624b10c..255a1387 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,12 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.12.6 + +### Fixed + +* fix: used finder->exclude instead of notPath for excludedDirs by @seizan8 in https://github.com/php-translation/symfony-bundle/pull/474 + ## 0.12.5 ### Added From 692900ef8870f112dba99c5a68680af5140faada Mon Sep 17 00:00:00 2001 From: robbedg Date: Fri, 2 Sep 2022 17:30:02 +0200 Subject: [PATCH 205/234] fix: check if session if available (#478) Co-authored-by: Robbe De Geyndt --- EditInPlace/Activator.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/EditInPlace/Activator.php b/EditInPlace/Activator.php index 3184f6c0..8289c594 100644 --- a/EditInPlace/Activator.php +++ b/EditInPlace/Activator.php @@ -50,10 +50,10 @@ public function setSession(Session $session): void /** * Get session based on availability. */ - private function getSession(): Session + private function getSession(): ?Session { $session = $this->session; - if (null === $session) { + if (null === $session && $this->requestStack->getCurrentRequest()->hasSession()) { $session = $this->requestStack->getSession(); } @@ -65,7 +65,9 @@ private function getSession(): Session */ public function activate(): void { - $this->getSession()->set(self::KEY, true); + if (null !== $this->getSession()) { + $this->getSession()->set(self::KEY, true); + } } /** @@ -73,7 +75,9 @@ public function activate(): void */ public function deactivate(): void { - $this->getSession()->remove(self::KEY); + if (null !== $this->getSession()) { + $this->getSession()->remove(self::KEY); + } } /** @@ -81,7 +85,7 @@ public function deactivate(): void */ public function checkRequest(Request $request = null): bool { - if (!$this->getSession()->has(self::KEY)) { + if (null === $this->getSession() || !$this->getSession()->has(self::KEY)) { return false; } From 123dfd27f5fb330d2ea5e5519565eae97f91839e Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Fri, 2 Sep 2022 18:33:16 +0300 Subject: [PATCH 206/234] Update Changelog.md --- Changelog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Changelog.md b/Changelog.md index 255a1387..075c239f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,12 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.12.7 + +### Fixed + +* fix: check if session is available by @robbedg in https://github.com/php-translation/symfony-bundle/pull/478 + ## 0.12.6 ### Fixed From 4bdd4cf7994da392ae9678de3a4f38f19a4db06e Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Sat, 3 Sep 2022 11:12:13 +0300 Subject: [PATCH 207/234] Fix code styles (#479) --- Catalogue/Operation/ReplaceOperation.php | 4 ++-- Tests/Functional/Controller/WebUIControllerTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Catalogue/Operation/ReplaceOperation.php b/Catalogue/Operation/ReplaceOperation.php index bdf277e2..6267876b 100644 --- a/Catalogue/Operation/ReplaceOperation.php +++ b/Catalogue/Operation/ReplaceOperation.php @@ -140,12 +140,12 @@ private function doMergeMetadata(array $source, array $target): array // If both arrays, do recursive call $source[$key] = $this->doMergeMetadata($source[$key], $value); } - // Else, use value form $source + // Else, use value form $source } else { // Add new value $source[$key] = $value; } - // if sequential + // if sequential } elseif (!\in_array($value, $source, true)) { $source[] = $value; } diff --git a/Tests/Functional/Controller/WebUIControllerTest.php b/Tests/Functional/Controller/WebUIControllerTest.php index d8b31915..6e1a4cb1 100644 --- a/Tests/Functional/Controller/WebUIControllerTest.php +++ b/Tests/Functional/Controller/WebUIControllerTest.php @@ -40,7 +40,7 @@ public static function setUpBeforeClass(): void XML - ); + ); } protected function setUp(): void From 4051a5be9154614807ef2e819a7fcc169dda1392 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Fri, 9 Sep 2022 16:19:40 +0300 Subject: [PATCH 208/234] Upgrade the SymfonyBundleTest (#481) * Upgrade the SymfonyBundleTest * Fix code styles * Use either new storage_factory_id or legacy storage_id * Fix tests in EditInPlaceTest * Fix code styles * Introduce legacy config in one more spot * Ignore 29 direct deprecations to make tests green --- EditInPlace/Activator.php | 3 +- Model/SfProfilerMessage.php | 3 -- Tests/Functional/BaseTestCase.php | 26 +++++++++------ Tests/Functional/BundleInitializationTest.php | 6 ++-- .../Catalogue/CatalogueFetcherTest.php | 7 ++-- .../Command/CheckMissingCommandTest.php | 6 ++-- .../Functional/Command/ExtractCommandTest.php | 10 +++--- .../Functional/Command/StatusCommandTest.php | 9 +++--- Tests/Functional/Command/SyncCommandTest.php | 9 +++--- .../Controller/EditInPlaceControllerTest.php | 7 ++-- .../Functional/Controller/EditInPlaceTest.php | 32 +++++++++++++------ .../Controller/WebUIControllerTest.php | 21 ++++++------ .../app/Service/DummyHttpClient.php | 5 +-- .../app/Service/DummyMessageFactory.php | 4 +-- .../Functional/app/config/default_legacy.yaml | 10 ++++++ .../app/config/disabled_label_legacy.yaml | 6 ++++ Tests/Functional/app/config/framework.yaml | 2 +- .../app/config/framework_legacy.yaml | 17 ++++++++++ composer.json | 2 +- phpunit.xml.dist | 3 +- 20 files changed, 123 insertions(+), 65 deletions(-) create mode 100644 Tests/Functional/app/config/default_legacy.yaml create mode 100644 Tests/Functional/app/config/disabled_label_legacy.yaml create mode 100644 Tests/Functional/app/config/framework_legacy.yaml diff --git a/EditInPlace/Activator.php b/EditInPlace/Activator.php index 8289c594..19fbc478 100644 --- a/EditInPlace/Activator.php +++ b/EditInPlace/Activator.php @@ -53,7 +53,8 @@ public function setSession(Session $session): void private function getSession(): ?Session { $session = $this->session; - if (null === $session && $this->requestStack->getCurrentRequest()->hasSession()) { + $request = $this->requestStack->getCurrentRequest(); + if (null === $session && $request && $request->hasSession()) { $session = $this->requestStack->getSession(); } diff --git a/Model/SfProfilerMessage.php b/Model/SfProfilerMessage.php index e5c597ed..e482942e 100644 --- a/Model/SfProfilerMessage.php +++ b/Model/SfProfilerMessage.php @@ -80,9 +80,6 @@ final class SfProfilerMessage */ private $parameters; - /** - * @return SfProfilerMessage - */ public static function create(array $data): self { $message = new self(); diff --git a/Tests/Functional/BaseTestCase.php b/Tests/Functional/BaseTestCase.php index 0aa04abf..84de6816 100644 --- a/Tests/Functional/BaseTestCase.php +++ b/Tests/Functional/BaseTestCase.php @@ -11,21 +11,22 @@ namespace Translation\Bundle\Tests\Functional; -use Nyholm\BundleTest\AppKernel; -use Nyholm\BundleTest\BaseBundleTestCase; +use Nyholm\BundleTest\TestKernel; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\TwigBundle\TwigBundle; +use Symfony\Component\HttpKernel\Kernel; use Translation\Bundle\TranslationBundle; /** * @author Tobias Nyholm */ -abstract class BaseTestCase extends BaseBundleTestCase +abstract class BaseTestCase extends KernelTestCase { /** - * @var AppKernel + * @var TestKernel */ - protected $kernel; + protected $testKernel; protected function getBundleClass(): string { @@ -34,13 +35,18 @@ protected function getBundleClass(): string protected function setUp(): void { - $kernel = $this->createKernel(); - $kernel->addConfigFile(__DIR__.'/app/config/default.yaml'); + $kernel = self::createKernel(); - $kernel->addBundle(TwigBundle::class); - $kernel->addBundle(TranslationBundle::class); + if (Kernel::VERSION_ID < 50300) { + $kernel->addTestConfig(__DIR__.'/app/config/default_legacy.yaml'); + } else { + $kernel->addTestConfig(__DIR__.'/app/config/default.yaml'); + } - $this->kernel = $kernel; + $kernel->addTestBundle(TwigBundle::class); + $kernel->addTestBundle(TranslationBundle::class); + + $this->testKernel = $kernel; parent::setUp(); } diff --git a/Tests/Functional/BundleInitializationTest.php b/Tests/Functional/BundleInitializationTest.php index 01570a90..625004ae 100644 --- a/Tests/Functional/BundleInitializationTest.php +++ b/Tests/Functional/BundleInitializationTest.php @@ -21,8 +21,10 @@ class BundleInitializationTest extends BaseTestCase { public function testRegisterBundle(): void { - $this->bootKernel(); - $container = $this->getContainer(); + $kernel = $this->testKernel; + $kernel->boot(); + $container = $kernel->getContainer(); + $this->assertTrue($container->has(ConfigurationManager::class)); $config = $container->get(ConfigurationManager::class); $this->assertInstanceOf(ConfigurationManager::class, $config); diff --git a/Tests/Functional/Catalogue/CatalogueFetcherTest.php b/Tests/Functional/Catalogue/CatalogueFetcherTest.php index 2dcaf816..234fb069 100644 --- a/Tests/Functional/Catalogue/CatalogueFetcherTest.php +++ b/Tests/Functional/Catalogue/CatalogueFetcherTest.php @@ -52,9 +52,8 @@ public static function setUpBeforeClass(): void public function testFetchCatalogue(): void { - $this->bootKernel(); - - $this->catalogueFetcher = $this->getContainer()->get(CatalogueFetcher::class); + $this->testKernel->boot(); + $this->catalogueFetcher = $this->testKernel->getContainer()->get(CatalogueFetcher::class); $data = self::getDefaultData(); $data['external_translations_dirs'] = [__DIR__.'/../app/Resources/translations/']; @@ -89,6 +88,6 @@ protected function setUp(): void { parent::setUp(); - $this->kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yaml'); + $this->testKernel->addTestConfig(__DIR__.'/../app/config/normal_config.yaml'); } } diff --git a/Tests/Functional/Command/CheckMissingCommandTest.php b/Tests/Functional/Command/CheckMissingCommandTest.php index d957f8de..59a2cb21 100644 --- a/Tests/Functional/Command/CheckMissingCommandTest.php +++ b/Tests/Functional/Command/CheckMissingCommandTest.php @@ -19,9 +19,9 @@ protected function setUp(): void { parent::setUp(); - $this->kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yaml'); - $this->bootKernel(); - $this->application = new Application($this->kernel); + $this->testKernel->addTestConfig(__DIR__.'/../app/config/normal_config.yaml'); + $this->testKernel->boot(); + $this->application = new Application($this->testKernel); file_put_contents( __DIR__.'/../app/Resources/translations/messages.sv.xlf', diff --git a/Tests/Functional/Command/ExtractCommandTest.php b/Tests/Functional/Command/ExtractCommandTest.php index ff7a9915..29574436 100644 --- a/Tests/Functional/Command/ExtractCommandTest.php +++ b/Tests/Functional/Command/ExtractCommandTest.php @@ -25,7 +25,8 @@ class ExtractCommandTest extends BaseTestCase protected function setUp(): void { parent::setUp(); - $this->kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yaml'); + + $this->testKernel->addTestConfig(__DIR__.'/../app/config/normal_config.yaml'); file_put_contents(__DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' @@ -67,10 +68,10 @@ protected function setUp(): void public function testExecute(): void { - $this->bootKernel(); - $application = new Application($this->kernel); + $this->testKernel->boot(); + $application = new Application($this->testKernel); - $container = $this->getContainer(); + $container = $this->testKernel->getContainer(); $application->add($container->get(ExtractCommand::class)); // transchoice tag have been definively removed in sf ^5.0 @@ -96,7 +97,6 @@ public function testExecute(): void $this->assertMatchesRegularExpression('|New messages +4|s', $output); $this->assertMatchesRegularExpression('|Total defined messages +8|s', $output); - $container = $this->getContainer(); $config = $container->get(ConfigurationManager::class)->getConfiguration('app'); $catalogues = $container->get(CatalogueFetcher::class)->getCatalogues($config, ['sv']); diff --git a/Tests/Functional/Command/StatusCommandTest.php b/Tests/Functional/Command/StatusCommandTest.php index 680f65f2..0733eb81 100644 --- a/Tests/Functional/Command/StatusCommandTest.php +++ b/Tests/Functional/Command/StatusCommandTest.php @@ -21,15 +21,16 @@ class StatusCommandTest extends BaseTestCase protected function setUp(): void { parent::setUp(); - $this->kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yaml'); + + $this->testKernel->addTestConfig(__DIR__.'/../app/config/normal_config.yaml'); } public function testExecute(): void { - $this->bootKernel(); - $application = new Application($this->kernel); + $this->testKernel->boot(); + $application = new Application($this->testKernel); - $container = $this->getContainer(); + $container = $this->testKernel->getContainer(); $application->add($container->get(StatusCommand::class)); $command = $application->find('translation:status'); diff --git a/Tests/Functional/Command/SyncCommandTest.php b/Tests/Functional/Command/SyncCommandTest.php index 72154d77..35cd97e9 100644 --- a/Tests/Functional/Command/SyncCommandTest.php +++ b/Tests/Functional/Command/SyncCommandTest.php @@ -21,7 +21,8 @@ class SyncCommandTest extends BaseTestCase protected function setUp(): void { parent::setUp(); - $this->kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yaml'); + + $this->testKernel->addTestConfig(__DIR__.'/../app/config/normal_config.yaml'); file_put_contents(__DIR__.'/../app/Resources/translations/messages.sv.xlf', <<<'XML' @@ -63,10 +64,10 @@ protected function setUp(): void public function testExecute(): void { - $this->bootKernel(); - $application = new Application($this->kernel); + $this->testKernel->boot(); + $application = new Application($this->testKernel); - $container = $this->getContainer(); + $container = $this->testKernel->getContainer(); $application->add($container->get(SyncCommand::class)); $command = $application->find('translation:sync'); diff --git a/Tests/Functional/Controller/EditInPlaceControllerTest.php b/Tests/Functional/Controller/EditInPlaceControllerTest.php index c2aa72c3..8d059ca9 100644 --- a/Tests/Functional/Controller/EditInPlaceControllerTest.php +++ b/Tests/Functional/Controller/EditInPlaceControllerTest.php @@ -49,7 +49,8 @@ public static function setUpBeforeClass(): void protected function setUp(): void { parent::setUp(); - $this->kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yaml'); + + $this->testKernel->addTestConfig(__DIR__.'/../app/config/normal_config.yaml'); } public function testEditAction(): void @@ -58,7 +59,7 @@ public function testEditAction(): void 'messages|key0' => 'trans0', 'messages|key1' => 'trans1', ])); - $response = $this->kernel->handle($request); + $response = $this->testKernel->handle($request); $this->assertEquals(200, $response->getStatusCode()); } @@ -68,7 +69,7 @@ public function testEditActionError(): void 'messages|key0' => 'trans0', 'messages|' => 'trans1', ])); - $response = $this->kernel->handle($request); + $response = $this->testKernel->handle($request); $this->assertEquals(400, $response->getStatusCode()); } } diff --git a/Tests/Functional/Controller/EditInPlaceTest.php b/Tests/Functional/Controller/EditInPlaceTest.php index aaec436d..b62e5d42 100644 --- a/Tests/Functional/Controller/EditInPlaceTest.php +++ b/Tests/Functional/Controller/EditInPlaceTest.php @@ -12,6 +12,9 @@ namespace Translation\Bundle\Tests\Functional\Controller; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; +use Symfony\Component\HttpKernel\Kernel; use Translation\Bundle\EditInPlace\Activator; use Translation\Bundle\Tests\Functional\BaseTestCase; @@ -22,13 +25,16 @@ class EditInPlaceTest extends BaseTestCase { public function testActivatedTest(): void { - $this->bootKernel(); + $this->testKernel->boot(); $request = Request::create('/foobar'); // Activate the feature - $this->getContainer()->get(Activator::class)->activate(); + $activator = $this->testKernel->getContainer()->get(Activator::class); + $session = new Session(new MockArraySessionStorage()); + $activator->setSession($session); + $activator->activate(); - $response = $this->kernel->handle($request); + $response = $this->testKernel->handle($request); self::assertSame(200, $response->getStatusCode()); self::assertStringContainsString('', $response->getContent()); @@ -53,14 +59,21 @@ public function testActivatedTest(): void public function testIfUntranslatableLabelGetsDisabled(): void { - $this->kernel->addConfigFile(__DIR__.'/../app/config/disabled_label.yaml'); + if (Kernel::VERSION_ID < 50300) { + $this->testKernel->addTestConfig(__DIR__.'/../app/config/disabled_label_legacy.yaml'); + } else { + $this->testKernel->addTestConfig(__DIR__.'/../app/config/disabled_label.yaml'); + } + $this->testKernel->boot(); $request = Request::create('/foobar'); // Activate the feature - $this->bootKernel(); - $this->getContainer()->get(Activator::class)->activate(); + $activator = $this->testKernel->getContainer()->get(Activator::class); + $session = new Session(new MockArraySessionStorage()); + $activator->setSession($session); + $activator->activate(); - $response = $this->kernel->handle($request); + $response = $this->testKernel->handle($request); self::assertSame(200, $response->getStatusCode()); self::assertStringContainsString('', $response->getContent()); @@ -85,9 +98,10 @@ public function testIfUntranslatableLabelGetsDisabled(): void public function testDeactivatedTest(): void { - $this->bootKernel(); + $this->testKernel->boot(); + $request = Request::create('/foobar'); - $response = $this->kernel->handle($request); + $response = $this->testKernel->handle($request); self::assertSame(200, $response->getStatusCode()); self::assertStringNotContainsString('x-trans', $response->getContent()); diff --git a/Tests/Functional/Controller/WebUIControllerTest.php b/Tests/Functional/Controller/WebUIControllerTest.php index 6e1a4cb1..bd134b90 100644 --- a/Tests/Functional/Controller/WebUIControllerTest.php +++ b/Tests/Functional/Controller/WebUIControllerTest.php @@ -46,24 +46,25 @@ public static function setUpBeforeClass(): void protected function setUp(): void { parent::setUp(); - $this->kernel->addConfigFile(__DIR__.'/../app/config/normal_config.yaml'); + + $this->testKernel->addTestConfig(__DIR__.'/../app/config/normal_config.yaml'); } public function testIndexAction(): void { $request = Request::create('/_trans', 'GET'); - $response = $this->kernel->handle($request); + $response = $this->testKernel->handle($request); $this->assertEquals(200, $response->getStatusCode()); $request = Request::create('/_trans/app', 'GET'); - $response = $this->kernel->handle($request); + $response = $this->testKernel->handle($request); $this->assertEquals(200, $response->getStatusCode()); } public function testShowAction(): void { $request = Request::create('/_trans/app/en/messages', 'GET'); - $response = $this->kernel->handle($request); + $response = $this->testKernel->handle($request); $this->assertEquals(200, $response->getStatusCode()); } @@ -72,14 +73,14 @@ public function testCreateAction(): void $request = Request::create('/_trans/app/sv/messages/new', 'POST', [], [], [], [], json_encode([ 'key' => 'foo', ])); - $response = $this->kernel->handle($request); + $response = $this->testKernel->handle($request); $this->assertEquals(400, $response->getStatusCode()); $request = Request::create('/_trans/app/sv/messages/new', 'POST', [], [], [], [], json_encode([ 'key' => 'foo', 'message' => 'bar', ])); - $response = $this->kernel->handle($request); + $response = $this->testKernel->handle($request); $this->assertEquals(200, $response->getStatusCode()); } @@ -88,14 +89,14 @@ public function testEditAction(): void $request = Request::create('/_trans/app/sv/messages', 'POST', [], [], [], [], json_encode([ 'key' => 'foo', ])); - $response = $this->kernel->handle($request); + $response = $this->testKernel->handle($request); $this->assertEquals(400, $response->getStatusCode()); $request = Request::create('/_trans/app/sv/messages', 'POST', [], [], [], [], json_encode([ 'key' => 'key1', 'message' => 'bar', ])); - $response = $this->kernel->handle($request); + $response = $this->testKernel->handle($request); $this->assertEquals(200, $response->getStatusCode()); } @@ -105,13 +106,13 @@ public function testDeleteAction(): void $request = Request::create('/_trans/app/sv/messages', 'DELETE', [], [], [], [], json_encode([ 'key' => 'empty', ])); - $response = $this->kernel->handle($request); + $response = $this->testKernel->handle($request); $this->assertEquals(200, $response->getStatusCode()); $request = Request::create('/_trans/app/sv/messages', 'DELETE', [], [], [], [], json_encode([ 'key' => 'foo', ])); - $response = $this->kernel->handle($request); + $response = $this->testKernel->handle($request); $this->assertEquals(200, $response->getStatusCode()); } } diff --git a/Tests/Functional/app/Service/DummyHttpClient.php b/Tests/Functional/app/Service/DummyHttpClient.php index c465796c..1851af4d 100644 --- a/Tests/Functional/app/Service/DummyHttpClient.php +++ b/Tests/Functional/app/Service/DummyHttpClient.php @@ -11,13 +11,14 @@ namespace Translation\Bundle\Tests\Functional\app\Service; -use GuzzleHttp\Psr7\Response; use Http\Client\HttpClient; +use Nyholm\Psr7\Response; use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; class DummyHttpClient implements HttpClient { - public function sendRequest(RequestInterface $request) + public function sendRequest(RequestInterface $request): ResponseInterface { return new Response(200); } diff --git a/Tests/Functional/app/Service/DummyMessageFactory.php b/Tests/Functional/app/Service/DummyMessageFactory.php index 6f44f697..d972edc8 100644 --- a/Tests/Functional/app/Service/DummyMessageFactory.php +++ b/Tests/Functional/app/Service/DummyMessageFactory.php @@ -11,9 +11,9 @@ namespace Translation\Bundle\Tests\Functional\app\Service; -use GuzzleHttp\Psr7\Request; -use GuzzleHttp\Psr7\Response; use Http\Message\MessageFactory; +use Nyholm\Psr7\Request; +use Nyholm\Psr7\Response; class DummyMessageFactory implements MessageFactory { diff --git a/Tests/Functional/app/config/default_legacy.yaml b/Tests/Functional/app/config/default_legacy.yaml new file mode 100644 index 00000000..368b40fd --- /dev/null +++ b/Tests/Functional/app/config/default_legacy.yaml @@ -0,0 +1,10 @@ +imports: + - { resource: parameters.php } + - { resource: framework_legacy.yaml } + - { resource: services.yaml } + +translation: + webui: + enabled: true + edit_in_place: + enabled: true diff --git a/Tests/Functional/app/config/disabled_label_legacy.yaml b/Tests/Functional/app/config/disabled_label_legacy.yaml new file mode 100644 index 00000000..1c088141 --- /dev/null +++ b/Tests/Functional/app/config/disabled_label_legacy.yaml @@ -0,0 +1,6 @@ +imports: + - { resource: default_legacy.yaml } + +translation: + edit_in_place: + show_untranslatable: false diff --git a/Tests/Functional/app/config/framework.yaml b/Tests/Functional/app/config/framework.yaml index 2d4a4226..034d29ba 100644 --- a/Tests/Functional/app/config/framework.yaml +++ b/Tests/Functional/app/config/framework.yaml @@ -2,7 +2,7 @@ framework: secret: test test: ~ session: - storage_id: session.storage.mock_file + storage_factory_id: session.storage.factory.mock_file form: false csrf_protection: false validation: diff --git a/Tests/Functional/app/config/framework_legacy.yaml b/Tests/Functional/app/config/framework_legacy.yaml new file mode 100644 index 00000000..2d4a4226 --- /dev/null +++ b/Tests/Functional/app/config/framework_legacy.yaml @@ -0,0 +1,17 @@ +framework: + secret: test + test: ~ + session: + storage_id: session.storage.mock_file + form: false + csrf_protection: false + validation: + enabled: true + router: + resource: "%test.project_dir%/config/routing.yaml" + type: 'yaml' + +twig: + strict_variables: "%kernel.debug%" #suppresses deprecation notices about the default value TwigBundle pre version 5 + paths: + "%test.project_dir%/Resources/views": App diff --git a/composer.json b/composer.json index 80eaee20..d17eaf9c 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "matthiasnoback/symfony-dependency-injection-test": "^4.1", "matthiasnoback/symfony-config-test": "^4.1", "nyholm/psr7": "^1.1", - "nyholm/symfony-bundle-test": "^1.6.1, <1.8" + "nyholm/symfony-bundle-test": "^2.0" }, "suggest": { "php-http/httplug-bundle": "To easier configure your httplug clients." diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8750475f..0a4e1492 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -12,7 +12,8 @@ > - + + From e35452fc38af3579644d3b4a939ddda7f7a1a225 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Mon, 19 Dec 2022 14:35:12 +0200 Subject: [PATCH 209/234] Apply changes suggested by PHP CS Fixer (#486) --- Tests/Functional/Controller/EditInPlaceTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Functional/Controller/EditInPlaceTest.php b/Tests/Functional/Controller/EditInPlaceTest.php index b62e5d42..932af74e 100644 --- a/Tests/Functional/Controller/EditInPlaceTest.php +++ b/Tests/Functional/Controller/EditInPlaceTest.php @@ -49,7 +49,7 @@ public function testActivatedTest(): void // Check attribute with prefix (href="mailto:...") $emailTag = $dom->getElementById('email'); - self::assertEquals('mailto:'.'🚫 Can\'t be translated here. 🚫', $emailTag->getAttribute('href')); + self::assertEquals('mailto:🚫 Can\'t be translated here. 🚫', $emailTag->getAttribute('href')); self::assertEquals('localized.email', $emailTag->textContent); // Check attribute From 9bd3ecace0a4019a7a4327ca9ea8df1c23ff0da3 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Mon, 19 Dec 2022 14:38:39 +0200 Subject: [PATCH 210/234] Prepare 0.12.8 (#485) * Prepare 0.12.8 * Update Changelog.md --- Changelog.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Changelog.md b/Changelog.md index 075c239f..da8ea81f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,17 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.12.8 + +### Fixed + +* Fix code styles by @bocharsky-bw in https://github.com/php-translation/symfony-bundle/pull/479 +* Apply changes suggested by PHP CS Fixer by @bocharsky-bw in https://github.com/php-translation/symfony-bundle/pull/486 + +### Changed + +* Upgrade the SymfonyBundleTest by @bocharsky-bw in https://github.com/php-translation/symfony-bundle/pull/481 + ## 0.12.7 ### Fixed From 9e2dda31ed9fdb16a3237304d57db8d625cf8d42 Mon Sep 17 00:00:00 2001 From: Marek Snopkowski Date: Mon, 23 Jan 2023 10:59:52 +0100 Subject: [PATCH 211/234] Bundle does not work properly with new Intl ICU translations (#452) * Remove both "normal" and ICU domain from messages set https://github.com/php-translation/symfony-bundle/issues/300 * Add new configuration for new message format It will allow us to control if we want to use new ICU format (with suffixed domain with "+intl-icu") or format. Configuration has normalizer (making sure string is in lower case) and validator (making sure it's a string and either "" or "icu"). Setting `new_message_format: ""` we'll end up with new translations in container like: {domain}.{locale}.{ext}. Setting `new_message_format: "icu"` we'll end up with new translations in container like: {domain}+intl-icu.{locale}.{ext}. https://github.com/php-translation/symfony-bundle/issues/300 * Add new field and getter based on recently added new config option https://github.com/php-translation/symfony-bundle/issues/300 * Make sure importer is ICU format aware Add new argument to converter methods to make sure we're able to recognize existing translations (and use correct domain - either standard or ICU format). In case translation is new we're going to use predefined format (using new configuration option new_message_format). https://github.com/php-translation/symfony-bundle/issues/300 * Make sure new_message_format configuration is defined Let the default configuration overlap with Symfony configuration option default value. https://github.com/php-translation/symfony-bundle/issues/300 * Pass to converter new_message_format configuration https://github.com/php-translation/symfony-bundle/issues/300 * Make sure importer is aware of ICU domain format Because of MessageCatalogue interface limitation we're using NSA to access raw messages data. Additionally because defines() method uses isset() were force to check raw messages data directly to check if key exits (translations from source collection are all set to NULL). https://github.com/php-translation/symfony-bundle/issues/300 * Let code "breath" a bit https://github.com/php-translation/symfony-bundle/issues/300 * Make sure we're always aware of ICU domain https://github.com/php-translation/symfony-bundle/issues/300 * Make sure we're aware of 3 different message domains We have: 1. Source message domain. 2. Target message domain. 3. Result message domain (this is tied to 2 of the above). This solution assumes we'd always like to use ICU format (result message domain) if we use it in any of source or target catalogue. Additionally because of default NULL values in source catalogue (internal extractor design to set NULL for "target" when creating messages catalogue from source collection) we have to check if desired catalogue's messages field has a key (within requested domain) or not. Using MessageCatalogue::defines() would return false for catalogue without translation (NULL value). Lastly - make sure internal messages field utilised result message domain for consistency with result field. https://github.com/php-translation/symfony-bundle/issues/300 * Persist PHP CS Fixer changes https://github.com/php-translation/symfony-bundle/issues/300 * Remove both "normal" and ICU domain from messages set https://github.com/php-translation/symfony-bundle/issues/300 * Add new configuration for new message format It will allow us to control if we want to use new ICU format (with suffixed domain with "+intl-icu") or format. Configuration has normalizer (making sure string is in lower case) and validator (making sure it's a string and either "" or "icu"). Setting `new_message_format: ""` we'll end up with new translations in container like: {domain}.{locale}.{ext}. Setting `new_message_format: "icu"` we'll end up with new translations in container like: {domain}+intl-icu.{locale}.{ext}. https://github.com/php-translation/symfony-bundle/issues/300 * Add new field and getter based on recently added new config option https://github.com/php-translation/symfony-bundle/issues/300 * Make sure importer is ICU format aware Add new argument to converter methods to make sure we're able to recognize existing translations (and use correct domain - either standard or ICU format). In case translation is new we're going to use predefined format (using new configuration option new_message_format). https://github.com/php-translation/symfony-bundle/issues/300 * Make sure new_message_format configuration is defined Let the default configuration overlap with Symfony configuration option default value. https://github.com/php-translation/symfony-bundle/issues/300 * Pass to converter new_message_format configuration https://github.com/php-translation/symfony-bundle/issues/300 * Make sure importer is aware of ICU domain format Because of MessageCatalogue interface limitation we're using NSA to access raw messages data. Additionally because defines() method uses isset() were force to check raw messages data directly to check if key exits (translations from source collection are all set to NULL). https://github.com/php-translation/symfony-bundle/issues/300 * Let code "breath" a bit https://github.com/php-translation/symfony-bundle/issues/300 * Make sure we're always aware of ICU domain https://github.com/php-translation/symfony-bundle/issues/300 * Make sure we're aware of 3 different message domains We have: 1. Source message domain. 2. Target message domain. 3. Result message domain (this is tied to 2 of the above). This solution assumes we'd always like to use ICU format (result message domain) if we use it in any of source or target catalogue. Additionally because of default NULL values in source catalogue (internal extractor design to set NULL for "target" when creating messages catalogue from source collection) we have to check if desired catalogue's messages field has a key (within requested domain) or not. Using MessageCatalogue::defines() would return false for catalogue without translation (NULL value). Lastly - make sure internal messages field utilised result message domain for consistency with result field. https://github.com/php-translation/symfony-bundle/issues/300 * Persist PHP CS Fixer changes https://github.com/php-translation/symfony-bundle/issues/300 * Apply PHP CS fixer automated fix * Fix main PHPUnit issue We're now providing getter method name for new field. * Fix catalogue counter * Make assertion more wordy * Update configuration to have passing tests Add new ICU messages domain. Make sure configuration is set to before-the-modification configuration. New messages will not be added to ICU domain. An additional tests must be run with new option set to ICU. This however may require functional tests refactor. * Apply PHP-CS-Fixer suggestion --- Catalogue/CatalogueCounter.php | 4 +- Catalogue/CatalogueFetcher.php | 4 +- Catalogue/Operation/ReplaceOperation.php | 50 +++++++++------- Command/CheckMissingCommand.php | 1 + Command/ExtractCommand.php | 1 + DependencyInjection/Configuration.php | 22 +++++++ Model/Configuration.php | 15 +++++ Service/Importer.php | 57 ++++++++++++++----- .../Catalogue/CatalogueFetcherTest.php | 1 + .../Functional/Command/ExtractCommandTest.php | 4 +- .../Functional/Command/StatusCommandTest.php | 6 +- .../app/Resources/translations/.gitignore | 1 + .../translations/messages+intl-icu.en.xlf | 27 +++++++++ .../Functional/app/config/normal_config.yaml | 2 + Tests/Unit/Model/ConfigurationTest.php | 1 + 15 files changed, 154 insertions(+), 42 deletions(-) create mode 100644 Tests/Functional/app/Resources/translations/messages+intl-icu.en.xlf diff --git a/Catalogue/CatalogueCounter.php b/Catalogue/CatalogueCounter.php index f3636156..f1b4747b 100644 --- a/Catalogue/CatalogueCounter.php +++ b/Catalogue/CatalogueCounter.php @@ -57,7 +57,9 @@ public function getCatalogueStatistics(MessageCatalogueInterface $catalogue): ar $result[$domain]['approved'] = 0; foreach ($catalogue->all($domain) as $key => $text) { - $metadata = new Metadata($catalogue->getMetadata($key, $domain)); + $intlDomain = $domain.'+intl-icu' /* MessageCatalogueInterface::INTL_DOMAIN_SUFFIX */; + $rawMetadata = $catalogue->getMetadata($key, $domain) ?: $catalogue->getMetadata($key, $intlDomain); + $metadata = new Metadata($rawMetadata); $state = $metadata->getState(); if ('new' === $state) { ++$result[$domain]['new']; diff --git a/Catalogue/CatalogueFetcher.php b/Catalogue/CatalogueFetcher.php index 504d305e..ec83b0b4 100644 --- a/Catalogue/CatalogueFetcher.php +++ b/Catalogue/CatalogueFetcher.php @@ -53,8 +53,8 @@ public function getCatalogues(Configuration $config, array $locales = []): array foreach ($currentCatalogue->getDomains() as $domain) { if (!$this->isValidDomain($config, $domain)) { - $messages = $currentCatalogue->all(); - unset($messages[$domain]); + $messages = NSA::getProperty($currentCatalogue, 'messages'); + unset($messages[$domain], $messages[$domain.'+intl-icu' /* MessageCatalogueInterface::INTL_DOMAIN_SUFFIX */]); NSA::setProperty($currentCatalogue, 'messages', $messages); } } diff --git a/Catalogue/Operation/ReplaceOperation.php b/Catalogue/Operation/ReplaceOperation.php index 6267876b..746a6861 100644 --- a/Catalogue/Operation/ReplaceOperation.php +++ b/Catalogue/Operation/ReplaceOperation.php @@ -11,6 +11,7 @@ namespace Translation\Bundle\Catalogue\Operation; +use Nyholm\NSA; use Symfony\Component\Translation\Catalogue\AbstractOperation; use Symfony\Component\Translation\MessageCatalogueInterface; use Symfony\Component\Translation\MetadataAwareInterface; @@ -37,26 +38,31 @@ protected function processDomain($domain): void 'new' => [], 'obsolete' => [], ]; - if (\defined(sprintf('%s::INTL_DOMAIN_SUFFIX', MessageCatalogueInterface::class))) { - $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; - } else { - $intlDomain = $domain; - } + + $intlDomain = $domain.'+intl-icu' /* MessageCatalogueInterface::INTL_DOMAIN_SUFFIX */; + + $sourceMessages = NSA::getProperty($this->source, 'messages'); + $targetMessages = NSA::getProperty($this->target, 'messages'); foreach ($this->source->all($domain) as $id => $message) { - $messageDomain = $this->source->defines($id, $intlDomain) ? $intlDomain : $domain; + $sourceIdInIntl = \array_key_exists($id, $sourceMessages[$intlDomain] ?? []); + $targetIdInIntl = \array_key_exists($id, $targetMessages[$intlDomain] ?? []); + + $sourceMessageDomain = $sourceIdInIntl ? $intlDomain : $domain; + $targetMessageDomain = $targetIdInIntl ? $intlDomain : $domain; + $resultMessageDomain = $sourceIdInIntl || $targetIdInIntl ? $intlDomain : $domain; if (!$this->target->has($id, $domain)) { // No merge required $translation = $message; - $this->messages[$domain]['new'][$id] = $message; - $resultMeta = $this->getMetadata($this->source, $messageDomain, $id); + $this->messages[$resultMessageDomain]['new'][$id] = $message; + $resultMeta = $this->getMetadata($this->source, $sourceMessageDomain, $id); } else { // Merge required - $translation = $message ?? $this->target->get($id, $domain); + $translation = $message ?? $this->target->get($id, $targetMessageDomain); $resultMeta = null; - $sourceMeta = $this->getMetadata($this->source, $messageDomain, $id); - $targetMeta = $this->getMetadata($this->target, $this->target->defines($id, $intlDomain) ? $intlDomain : $domain, $id); + $sourceMeta = $this->getMetadata($this->source, $sourceMessageDomain, $id); + $targetMeta = $this->getMetadata($this->target, $targetMessageDomain, $id); if (\is_array($sourceMeta) && \is_array($targetMeta)) { // We can only merge meta if both is an array $resultMeta = $this->mergeMetadata($sourceMeta, $targetMeta); @@ -68,11 +74,11 @@ protected function processDomain($domain): void } } - $this->messages[$domain]['all'][$id] = $translation; - $this->result->add([$id => $translation], $messageDomain); + $this->messages[$resultMessageDomain]['all'][$id] = $translation; + $this->result->add([$id => $translation], $resultMessageDomain); if (!empty($resultMeta)) { - $this->result->setMetadata($id, $resultMeta, $messageDomain); + $this->result->setMetadata($id, $resultMeta, $resultMessageDomain); } } @@ -83,14 +89,18 @@ protected function processDomain($domain): void continue; } - $messageDomain = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain; - $this->messages[$domain]['all'][$id] = $message; - $this->messages[$domain]['obsolete'][$id] = $message; - $this->result->add([$id => $message], $messageDomain); + $sourceIdInIntl = \array_key_exists($id, $sourceMessages[$intlDomain] ?? []); + $targetIdInIntl = \array_key_exists($id, $targetMessages[$intlDomain] ?? []); + + $resultMessageDomain = $sourceIdInIntl || $targetIdInIntl ? $intlDomain : $domain; + + $this->messages[$resultMessageDomain]['all'][$id] = $message; + $this->messages[$resultMessageDomain]['obsolete'][$id] = $message; + $this->result->add([$id => $message], $resultMessageDomain); - $resultMeta = $this->getMetadata($this->target, $messageDomain, $id); + $resultMeta = $this->getMetadata($this->target, $resultMessageDomain, $id); if (!empty($resultMeta)) { - $this->result->setMetadata($id, $resultMeta, $messageDomain); + $this->result->setMetadata($id, $resultMeta, $resultMessageDomain); } } } diff --git a/Command/CheckMissingCommand.php b/Command/CheckMissingCommand.php index 46a09e09..58a6ac4d 100644 --- a/Command/CheckMissingCommand.php +++ b/Command/CheckMissingCommand.php @@ -80,6 +80,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'blacklist_domains' => $config->getBlacklistDomains(), 'whitelist_domains' => $config->getWhitelistDomains(), 'project_root' => $config->getProjectRoot(), + 'new_message_format' => $config->getNewMessageFormat(), ] ); diff --git a/Command/ExtractCommand.php b/Command/ExtractCommand.php index 7dd27bfd..f61290dd 100644 --- a/Command/ExtractCommand.php +++ b/Command/ExtractCommand.php @@ -106,6 +106,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'blacklist_domains' => $config->getBlacklistDomains(), 'whitelist_domains' => $config->getWhitelistDomains(), 'project_root' => $config->getProjectRoot(), + 'new_message_format' => $config->getNewMessageFormat(), ]); $errors = $result->getErrors(); diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 518eea73..0de8fd2a 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -164,6 +164,28 @@ private function configsNode(ArrayNodeDefinition $root): void ->scalarNode('output_dir')->cannotBeEmpty()->defaultValue('%kernel.project_dir%/Resources/translations')->end() ->scalarNode('project_root')->info("The root dir of your project. By default this will be kernel_root's parent.")->end() ->scalarNode('xliff_version')->info('The version of XLIFF XML you want to use (if dumping to this format).')->defaultValue('2.0')->end() + ->scalarNode('new_message_format') + ->info('Use "icu" to place new translations in "{domain}+intl-icu.{locale}.{ext}" container') + ->defaultValue('icu') + ->beforeNormalization() + ->ifTrue( + function ($format) { + return \is_string($format); + } + ) + ->then( + function (string $format) { + return strtolower($format); + } + ) + ->end() + ->validate() + ->ifTrue(function ($value) { + return !\is_string($value) || !\in_array($value, ['', 'icu']); + }) + ->thenInvalid('The "new_message_format" must be either: "" or "icu"; got "%s"') + ->end() + ->end() ->variableNode('local_file_storage_options') ->info('Options passed to the local file storage\'s dumper.') ->defaultValue([]) diff --git a/Model/Configuration.php b/Model/Configuration.php index e4f7bdac..20b25138 100644 --- a/Model/Configuration.php +++ b/Model/Configuration.php @@ -92,6 +92,11 @@ final class Configuration */ private $xliffVersion; + /** + * @var string + */ + private $newMessageFormat; + public function __construct(array $data) { $this->name = $data['name']; @@ -106,6 +111,7 @@ public function __construct(array $data) $this->blacklistDomains = $data['blacklist_domains']; $this->whitelistDomains = $data['whitelist_domains']; $this->xliffVersion = $data['xliff_version']; + $this->newMessageFormat = $data['new_message_format']; } public function getName(): string @@ -177,6 +183,15 @@ public function getXliffVersion(): string return $this->xliffVersion; } + /** + * If set to "icu" it'll place all new translations in "{domain}+intl-icu.{locale}.{ext}" file. + * Otherwise normal "{domain}.{locale}.{ext}" file will be used. + */ + public function getNewMessageFormat(): string + { + return $this->newMessageFormat; + } + /** * Reconfigures the directories so we can use one configuration for extracting * the messages only from one bundle. diff --git a/Service/Importer.php b/Service/Importer.php index 146add31..75ca10ed 100644 --- a/Service/Importer.php +++ b/Service/Importer.php @@ -11,6 +11,7 @@ namespace Translation\Bundle\Service; +use Nyholm\NSA; use Symfony\Component\Finder\Finder; use Symfony\Component\Translation\MessageCatalogue; use Translation\Bundle\Catalogue\Operation\ReplaceOperation; @@ -74,11 +75,11 @@ public function extractToCatalogues(Finder $finder, array $catalogues, array $co $results = []; foreach ($catalogues as $catalogue) { $target = new MessageCatalogue($catalogue->getLocale()); - $this->convertSourceLocationsToMessages($target, $sourceCollection); + $this->convertSourceLocationsToMessages($target, $sourceCollection, $catalogue); // Remove all SourceLocation and State form catalogue. - foreach ($catalogue->getDomains() as $domain) { - foreach ($catalogue->all($domain) as $key => $translation) { + foreach (NSA::getProperty($catalogue, 'messages') as $domain => $translations) { + foreach ($translations as $key => $translation) { $meta = $this->getMetadata($catalogue, $key, $domain); $meta->removeAllInCategory('file-source'); $meta->removeAllInCategory('state'); @@ -90,28 +91,37 @@ public function extractToCatalogues(Finder $finder, array $catalogues, array $co $result = $merge->getResult(); $domains = $merge->getDomains(); + $resultMessages = NSA::getProperty($result, 'messages'); + // Mark new messages as new/obsolete foreach ($domains as $domain) { + $intlDomain = $domain.'+intl-icu' /* MessageCatalogueInterface::INTL_DOMAIN_SUFFIX */; + foreach ($merge->getNewMessages($domain) as $key => $translation) { - $meta = $this->getMetadata($result, $key, $domain); + $messageDomain = \array_key_exists($key, $resultMessages[$intlDomain] ?? []) ? $intlDomain : $domain; + + $meta = $this->getMetadata($result, $key, $messageDomain); $meta->setState('new'); - $this->setMetadata($result, $key, $domain, $meta); + $this->setMetadata($result, $key, $messageDomain, $meta); // Add custom translations that we found in the source if (null === $translation) { if (null !== $newTranslation = $meta->getTranslation()) { - $result->set($key, $newTranslation, $domain); + $result->set($key, $newTranslation, $messageDomain); // We do not want "translation" key stored anywhere. $meta->removeAllInCategory('translation'); } elseif (null !== ($newTranslation = $meta->getDesc()) && $catalogue->getLocale() === $this->defaultLocale) { - $result->set($key, $newTranslation, $domain); + $result->set($key, $newTranslation, $messageDomain); } } } + foreach ($merge->getObsoleteMessages($domain) as $key => $translation) { - $meta = $this->getMetadata($result, $key, $domain); + $messageDomain = \array_key_exists($key, $resultMessages[$intlDomain] ?? []) ? $intlDomain : $domain; + + $meta = $this->getMetadata($result, $key, $messageDomain); $meta->setState('obsolete'); - $this->setMetadata($result, $key, $domain, $meta); + $this->setMetadata($result, $key, $messageDomain, $meta); } } $results[] = $result; @@ -120,22 +130,40 @@ public function extractToCatalogues(Finder $finder, array $catalogues, array $co return new ImportResult($results, $sourceCollection->getErrors()); } - private function convertSourceLocationsToMessages(MessageCatalogue $catalogue, SourceCollection $collection): void - { + private function convertSourceLocationsToMessages( + MessageCatalogue $catalogue, + SourceCollection $collection, + MessageCatalogue $currentCatalogue + ): void { + $currentMessages = NSA::getProperty($currentCatalogue, 'messages'); + /** @var SourceLocation $sourceLocation */ foreach ($collection as $sourceLocation) { $context = $sourceLocation->getContext(); $domain = $context['domain'] ?? 'messages'; + // Check with white/black list if (!$this->isValidDomain($domain)) { continue; } + $intlDomain = $domain.'+intl-icu' /* MessageCatalogueInterface::INTL_DOMAIN_SUFFIX */; + $key = $sourceLocation->getMessage(); - $catalogue->add([$key => null], $domain); + + if (\array_key_exists($key, $currentMessages[$intlDomain] ?? [])) { + $messageDomain = $intlDomain; + } elseif (\array_key_exists($key, $currentMessages[$domain] ?? [])) { + $messageDomain = $domain; + } else { + // New translation + $messageDomain = 'icu' === $this->config['new_message_format'] ? $intlDomain : $domain; + } + + $catalogue->add([$key => null], $messageDomain); $trimLength = 1 + \strlen($this->config['project_root']); - $meta = $this->getMetadata($catalogue, $key, $domain); + $meta = $this->getMetadata($catalogue, $key, $messageDomain); $meta->addCategory('file-source', sprintf('%s:%s', substr($sourceLocation->getPath(), $trimLength), $sourceLocation->getLine())); if (isset($sourceLocation->getContext()['desc'])) { $meta->addCategory('desc', $sourceLocation->getContext()['desc']); @@ -143,7 +171,7 @@ private function convertSourceLocationsToMessages(MessageCatalogue $catalogue, S if (isset($sourceLocation->getContext()['translation'])) { $meta->addCategory('translation', $sourceLocation->getContext()['translation']); } - $this->setMetadata($catalogue, $key, $domain, $meta); + $this->setMetadata($catalogue, $key, $messageDomain, $meta); } } @@ -178,6 +206,7 @@ private function processConfig(array $config): void 'project_root' => '', 'blacklist_domains' => [], 'whitelist_domains' => [], + 'new_message_format' => 'icu', ]; $config = array_merge($default, $config); diff --git a/Tests/Functional/Catalogue/CatalogueFetcherTest.php b/Tests/Functional/Catalogue/CatalogueFetcherTest.php index 234fb069..05f1026b 100644 --- a/Tests/Functional/Catalogue/CatalogueFetcherTest.php +++ b/Tests/Functional/Catalogue/CatalogueFetcherTest.php @@ -81,6 +81,7 @@ public static function getDefaultData(): array 'blacklist_domains' => ['getBlacklistDomains'], 'whitelist_domains' => ['getWhitelistDomains'], 'xliff_version' => ['getXliffVersion'], + 'new_message_format' => ['getNewMessageFormat'], ]; } diff --git a/Tests/Functional/Command/ExtractCommandTest.php b/Tests/Functional/Command/ExtractCommandTest.php index 29574436..e16f2ea8 100644 --- a/Tests/Functional/Command/ExtractCommandTest.php +++ b/Tests/Functional/Command/ExtractCommandTest.php @@ -111,9 +111,9 @@ public function testExecute(): void } $meta = new Metadata($catalogue->getMetadata('not.in.source')); - $this->assertTrue('obsolete' === $meta->getState()); + self::assertSame('obsolete', $meta->getState(), 'Expect meta state to be correct'); $meta = new Metadata($catalogue->getMetadata('translated.title')); - $this->assertTrue('new' === $meta->getState()); + self::assertSame('new', $meta->getState(), 'Expect meta state to be correct'); } } diff --git a/Tests/Functional/Command/StatusCommandTest.php b/Tests/Functional/Command/StatusCommandTest.php index 0733eb81..d113fefd 100644 --- a/Tests/Functional/Command/StatusCommandTest.php +++ b/Tests/Functional/Command/StatusCommandTest.php @@ -55,9 +55,9 @@ public function testExecute(): void $this->assertArrayHasKey('new', $total); $this->assertArrayHasKey('obsolete', $total); $this->assertArrayHasKey('approved', $total); - $this->assertEquals(2, $total['defined']); - $this->assertEquals(1, $total['new']); + $this->assertEquals(4, $total['defined']); + $this->assertEquals(2, $total['new']); $this->assertEquals(0, $total['obsolete']); - $this->assertEquals(1, $total['approved']); + $this->assertEquals(2, $total['approved']); } } diff --git a/Tests/Functional/app/Resources/translations/.gitignore b/Tests/Functional/app/Resources/translations/.gitignore index 1351b683..c716ed44 100644 --- a/Tests/Functional/app/Resources/translations/.gitignore +++ b/Tests/Functional/app/Resources/translations/.gitignore @@ -1,2 +1,3 @@ +messages+intl-icu.sv.xlf messages.sv.xlf *~ diff --git a/Tests/Functional/app/Resources/translations/messages+intl-icu.en.xlf b/Tests/Functional/app/Resources/translations/messages+intl-icu.en.xlf new file mode 100644 index 00000000..8885246b --- /dev/null +++ b/Tests/Functional/app/Resources/translations/messages+intl-icu.en.xlf @@ -0,0 +1,27 @@ + + + + + + new + true + user login + + + key2 + trans2 + + + + + Resources/views/translated.html.twig:12 + file-source + status:new + + + key3 + trans3 + + + + diff --git a/Tests/Functional/app/config/normal_config.yaml b/Tests/Functional/app/config/normal_config.yaml index fbc00d2e..bb83d961 100644 --- a/Tests/Functional/app/config/normal_config.yaml +++ b/Tests/Functional/app/config/normal_config.yaml @@ -9,7 +9,9 @@ translation: dirs: ["%test.project_dir%/Resources/views"] output_dir: "%test.project_dir%/Resources/translations" project_root: "%test.project_dir%" + new_message_format: '' app_with_transchoice: dirs: ["%test.project_dir%/Resources/views_with_transchoice"] output_dir: "%test.project_dir%/Resources/translations" project_root: "%test.project_dir%" + new_message_format: '' diff --git a/Tests/Unit/Model/ConfigurationTest.php b/Tests/Unit/Model/ConfigurationTest.php index 7160b491..effee503 100644 --- a/Tests/Unit/Model/ConfigurationTest.php +++ b/Tests/Unit/Model/ConfigurationTest.php @@ -56,6 +56,7 @@ public static function getDefaultData(): array 'blacklist_domains' => ['getBlacklistDomains'], 'whitelist_domains' => ['getWhitelistDomains'], 'xliff_version' => 'getXliffVersion', + 'new_message_format' => 'getNewMessageFormat', ]; } } From 1dbcbeb3503bd993fb996a8d572c9e130a9691a7 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Tue, 14 Feb 2023 13:11:01 +0200 Subject: [PATCH 212/234] Allow Composer to run php-http/discovery plugin (#489) --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d17eaf9c..3041ebd7 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,8 @@ }, "config": { "allow-plugins": { - "bamarni/composer-bin-plugin": true + "bamarni/composer-bin-plugin": true, + "php-http/discovery": true } } } From 3aa1761033f73ce98f2031b96316dd3e7ac3c465 Mon Sep 17 00:00:00 2001 From: krzyc Date: Tue, 14 Feb 2023 12:18:45 +0100 Subject: [PATCH 213/234] Fix Symfony 6.x compatibility (#488) Co-authored-by: bocharsky-bw --- Controller/SymfonyProfilerController.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Controller/SymfonyProfilerController.php b/Controller/SymfonyProfilerController.php index d36e54e7..b17f8502 100644 --- a/Controller/SymfonyProfilerController.php +++ b/Controller/SymfonyProfilerController.php @@ -166,8 +166,12 @@ protected function getSelectedMessages(Request $request, string $token): array { $this->getProfiler()->disable(); + $parameters = $request->request->all(); + if (!isset($parameters['selected'])) { + return []; + } /** @var string[] $selected */ - $selected = (array) $request->request->get('selected'); + $selected = (array) $parameters['selected']; if (0 === \count($selected)) { return []; } From be5558b3bc8cd1ee5f05f871851edbba9756413b Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Tue, 14 Feb 2023 13:26:31 +0200 Subject: [PATCH 214/234] Prepare release v0.13.0 (#487) * Update Changelog.md * Add more changes merged recently --- Changelog.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Changelog.md b/Changelog.md index da8ea81f..f17b726a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,16 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.13.0 + +### Added + +* Bundle does not work properly with new Intl ICU translations by @snpy in https://github.com/php-translation/symfony-bundle/pull/452 + +### Fixed + +* Symfony Profiler Fix Symfony 6.x compatibility by @krzyc in https://github.com/php-translation/symfony-bundle/pull/488 + ## 0.12.8 ### Fixed From d930db6015e0f1a05e4e2fe86e20f3191485cd99 Mon Sep 17 00:00:00 2001 From: Alexis Urien Date: Tue, 11 Jul 2023 10:49:12 -0700 Subject: [PATCH 215/234] Remove symfony console $defaultName deprecation (#491) * - Remove symfony console $defaultName deprecation and use AsCommand Annotation - Upgrade to PHP ^8.0 - Upgrade to symfony ^5.3 * Fix CI for PHP 8.0+ * Apply existing php-cs rules with php8+ * Fix CI --------- Co-authored-by: Alexis Urien --- .github/workflows/ci.yml | 6 ++--- .github/workflows/static.yml | 4 ++-- Catalogue/CatalogueManager.php | 12 +++++----- Catalogue/Operation/ReplaceOperation.php | 4 ++-- Command/BundleTrait.php | 2 +- Command/CheckMissingCommand.php | 7 +++--- Command/DeleteEmptyCommand.php | 7 +++--- Command/DeleteObsoleteCommand.php | 7 +++--- Command/DownloadCommand.php | 9 ++++---- Command/ExtractCommand.php | 7 +++--- Command/StatusCommand.php | 7 +++--- Command/SyncCommand.php | 9 ++++---- Controller/WebUIController.php | 2 +- DependencyInjection/Configuration.php | 3 --- DependencyInjection/TranslationExtension.php | 9 -------- EditInPlace/Activator.php | 5 +--- Service/CacheClearer.php | 2 +- Service/Importer.php | 8 +++---- Translator/FallbackTranslator.php | 15 ------------ Twig/EditInPlaceExtension.php | 6 ----- Twig/TranslationExtension.php | 12 +--------- composer.json | 24 ++++++++++---------- 22 files changed, 63 insertions(+), 104 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10f239ec..ba3cef4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,14 +10,12 @@ jobs: strategy: fail-fast: false matrix: - php: [ '7.2', '7.3', '7.4', '8.0', '8.1' ] + php: ['8.0', '8.1', '8.2'] strategy: [ 'highest' ] sf_version: [''] include: - - php: 7.4 + - php: 8.0 strategy: 'lowest' - - php: 7.3 - sf_version: '4.*' steps: - name: Checkout code diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index b4b5d41e..d5d853a0 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -22,7 +22,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.0' coverage: none - name: Download dependencies @@ -63,7 +63,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.0' coverage: none - name: Download dependencies diff --git a/Catalogue/CatalogueManager.php b/Catalogue/CatalogueManager.php index 5d554c0e..09a50d57 100644 --- a/Catalogue/CatalogueManager.php +++ b/Catalogue/CatalogueManager.php @@ -67,12 +67,12 @@ public function getMessages(string $locale, string $domain): array /** * @param array $config { * - * @var string $domain - * @var string $locale - * @var bool $isNew - * @var bool $isObsolete - * @var bool $isApproved - * } + * @var string $domain + * @var string $locale + * @var bool $isNew + * @var bool $isObsolete + * @var bool $isApproved + * } * * @return CatalogueMessage[] */ diff --git a/Catalogue/Operation/ReplaceOperation.php b/Catalogue/Operation/ReplaceOperation.php index 746a6861..5b012e5e 100644 --- a/Catalogue/Operation/ReplaceOperation.php +++ b/Catalogue/Operation/ReplaceOperation.php @@ -150,12 +150,12 @@ private function doMergeMetadata(array $source, array $target): array // If both arrays, do recursive call $source[$key] = $this->doMergeMetadata($source[$key], $value); } - // Else, use value form $source + // Else, use value form $source } else { // Add new value $source[$key] = $value; } - // if sequential + // if sequential } elseif (!\in_array($value, $source, true)) { $source[] = $value; } diff --git a/Command/BundleTrait.php b/Command/BundleTrait.php index cf8af0c0..c2ec27f6 100644 --- a/Command/BundleTrait.php +++ b/Command/BundleTrait.php @@ -20,7 +20,7 @@ trait BundleTrait private function configureBundleDirs(InputInterface $input, Configuration $config): void { if ($bundleName = $input->getOption('bundle')) { - if (0 === strpos($bundleName, '@')) { + if (str_starts_with($bundleName, '@')) { if (false === $pos = strpos($bundleName, '/')) { $bundleName = substr($bundleName, 1); } else { diff --git a/Command/CheckMissingCommand.php b/Command/CheckMissingCommand.php index 58a6ac4d..be95e47e 100644 --- a/Command/CheckMissingCommand.php +++ b/Command/CheckMissingCommand.php @@ -4,6 +4,7 @@ namespace Translation\Bundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -17,10 +18,11 @@ use Translation\Bundle\Service\ConfigurationManager; use Translation\Bundle\Service\Importer; +#[AsCommand( + name: 'translation:check-missing' +)] final class CheckMissingCommand extends Command { - protected static $defaultName = 'translation:check-missing'; - /** * @var ConfigurationManager */ @@ -58,7 +60,6 @@ public function __construct( protected function configure(): void { $this - ->setName(self::$defaultName) ->setDescription('Check that all translations for a given locale are extracted.') ->addArgument('locale', InputArgument::REQUIRED, 'The locale to check') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default'); diff --git a/Command/DeleteEmptyCommand.php b/Command/DeleteEmptyCommand.php index 3c40a997..75f290c9 100644 --- a/Command/DeleteEmptyCommand.php +++ b/Command/DeleteEmptyCommand.php @@ -11,6 +11,7 @@ namespace Translation\Bundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputArgument; @@ -26,13 +27,14 @@ /** * @author Tobias Nyholm */ +#[AsCommand( + name: 'translation:delete-empty' +)] class DeleteEmptyCommand extends Command { use BundleTrait; use StorageTrait; - protected static $defaultName = 'translation:delete-empty'; - /** * @var ConfigurationManager */ @@ -64,7 +66,6 @@ public function __construct( protected function configure(): void { $this - ->setName(self::$defaultName) ->setDescription('Delete all translations currently empty.') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') ->addArgument('locale', InputArgument::OPTIONAL, 'The locale to use. If omitted, we use all configured locales.', null) diff --git a/Command/DeleteObsoleteCommand.php b/Command/DeleteObsoleteCommand.php index 1132559c..74a04f73 100644 --- a/Command/DeleteObsoleteCommand.php +++ b/Command/DeleteObsoleteCommand.php @@ -11,6 +11,7 @@ namespace Translation\Bundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputArgument; @@ -26,13 +27,14 @@ /** * @author Tobias Nyholm */ +#[AsCommand( + name: 'translation:delete-obsolete' +)] class DeleteObsoleteCommand extends Command { use BundleTrait; use StorageTrait; - protected static $defaultName = 'translation:delete-obsolete'; - /** * @var ConfigurationManager */ @@ -64,7 +66,6 @@ public function __construct( protected function configure(): void { $this - ->setName(self::$defaultName) ->setDescription('Delete all translations marked as obsolete.') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') ->addArgument('locale', InputArgument::OPTIONAL, 'The locale to use. If omitted, we use all configured locales.', null) diff --git a/Command/DownloadCommand.php b/Command/DownloadCommand.php index d877afa8..f36d0535 100644 --- a/Command/DownloadCommand.php +++ b/Command/DownloadCommand.php @@ -11,6 +11,7 @@ namespace Translation\Bundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -26,13 +27,14 @@ /** * @author Tobias Nyholm */ +#[AsCommand( + name: 'translation:download' +)] class DownloadCommand extends Command { use BundleTrait; use StorageTrait; - protected static $defaultName = 'translation:download'; - private $configurationManager; private $cacheCleaner; private $catalogueWriter; @@ -54,7 +56,6 @@ public function __construct( protected function configure(): void { $this - ->setName(self::$defaultName) ->setDescription('Replace local messages with messages from remote') ->setHelp(<<%command.name% will erase all your local translations and replace them with translations downloaded from the remote. @@ -138,7 +139,7 @@ public function cleanParameters(array $raw) foreach ($raw as $string) { // Assert $string looks like "foo:bar" - list($key, $value) = explode(':', $string, 2); + [$key, $value] = explode(':', $string, 2); $config[$key][] = $value; } diff --git a/Command/ExtractCommand.php b/Command/ExtractCommand.php index f61290dd..488b58fa 100644 --- a/Command/ExtractCommand.php +++ b/Command/ExtractCommand.php @@ -11,6 +11,7 @@ namespace Translation\Bundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -29,12 +30,13 @@ /** * @author Tobias Nyholm */ +#[AsCommand( + name: 'translation:extract' +)] class ExtractCommand extends Command { use BundleTrait; - protected static $defaultName = 'translation:extract'; - /** * @var CatalogueFetcher */ @@ -79,7 +81,6 @@ public function __construct( protected function configure(): void { $this - ->setName(self::$defaultName) ->setDescription('Extract translations from source code.') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') ->addArgument('locale', InputArgument::OPTIONAL, 'The locale to use. If omitted, we use all configured locales.', false) diff --git a/Command/StatusCommand.php b/Command/StatusCommand.php index 73e21fa4..4c1a2bda 100644 --- a/Command/StatusCommand.php +++ b/Command/StatusCommand.php @@ -11,6 +11,7 @@ namespace Translation\Bundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -24,12 +25,13 @@ /** * @author Tobias Nyholm */ +#[AsCommand( + name: 'translation:status' +)] class StatusCommand extends Command { use BundleTrait; - protected static $defaultName = 'translation:status'; - /** * @var CatalogueCounter */ @@ -60,7 +62,6 @@ public function __construct( protected function configure(): void { $this - ->setName(self::$defaultName) ->setDescription('Show status about your translations.') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') ->addArgument('locale', InputArgument::OPTIONAL, 'The locale to use. If omitted, we use all configured locales.', false) diff --git a/Command/SyncCommand.php b/Command/SyncCommand.php index 859872ff..3da5af34 100644 --- a/Command/SyncCommand.php +++ b/Command/SyncCommand.php @@ -11,6 +11,7 @@ namespace Translation\Bundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -22,12 +23,13 @@ /** * @author Tobias Nyholm */ +#[AsCommand( + name: 'translation:sync' +)] class SyncCommand extends Command { use StorageTrait; - protected static $defaultName = 'translation:sync'; - public function __construct(StorageManager $storageManager) { $this->storageManager = $storageManager; @@ -38,7 +40,6 @@ public function __construct(StorageManager $storageManager) protected function configure(): void { $this - ->setName(self::$defaultName) ->setDescription('Sync the translations with the remote storage') ->addArgument('configuration', InputArgument::OPTIONAL, 'The configuration to use', 'default') ->addArgument('direction', InputArgument::OPTIONAL, 'Use "down" if local changes should be overwritten, otherwise "up"', 'down') @@ -78,7 +79,7 @@ public function cleanParameters(array $raw) foreach ($raw as $string) { // Assert $string looks like "foo:bar" - list($key, $value) = explode(':', $string, 2); + [$key, $value] = explode(':', $string, 2); $config[$key][] = $value; } diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index 2ce6de07..0604ba29 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -76,7 +76,7 @@ public function __construct( /** * Show a dashboard for the configuration. */ - public function indexAction(?string $configName = null): Response + public function indexAction(string $configName = null): Response { if (!$this->isWebUIEnabled) { return new Response('You are not allowed here. Check your config.', Response::HTTP_BAD_REQUEST); diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 0de8fd2a..95be8ca1 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -29,9 +29,6 @@ public function __construct(ContainerBuilder $container) $this->container = $container; } - /** - * {@inheritdoc} - */ public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('translation'); diff --git a/DependencyInjection/TranslationExtension.php b/DependencyInjection/TranslationExtension.php index 24424863..2f1b1774 100644 --- a/DependencyInjection/TranslationExtension.php +++ b/DependencyInjection/TranslationExtension.php @@ -37,9 +37,6 @@ */ class TranslationExtension extends Extension { - /** - * {@inheritdoc} - */ public function load(array $configs, ContainerBuilder $container): void { $container->setParameter('extractor_vendor_dir', $this->getExtractorVendorDirectory()); @@ -224,17 +221,11 @@ private function enableFallbackAutoTranslator(ContainerBuilder $container, array $container->setParameter('php_translation.translator_service.api_key', $config['fallback_translation']['api_key']); } - /** - * {@inheritdoc} - */ public function getAlias(): string { return 'translation'; } - /** - * {@inheritdoc} - */ public function getConfiguration(array $config, ContainerBuilder $container): Configuration { return new Configuration($container); diff --git a/EditInPlace/Activator.php b/EditInPlace/Activator.php index 19fbc478..b5c38583 100644 --- a/EditInPlace/Activator.php +++ b/EditInPlace/Activator.php @@ -32,7 +32,7 @@ final class Activator implements ActivatorInterface /** * @var Session|null */ - private $session = null; + private $session; public function __construct(RequestStack $requestStack) { @@ -81,9 +81,6 @@ public function deactivate(): void } } - /** - * {@inheritdoc} - */ public function checkRequest(Request $request = null): bool { if (null === $this->getSession() || !$this->getSession()->has(self::KEY)) { diff --git a/Service/CacheClearer.php b/Service/CacheClearer.php index 869ad637..6273a844 100644 --- a/Service/CacheClearer.php +++ b/Service/CacheClearer.php @@ -58,7 +58,7 @@ public function __construct(string $kernelCacheDir, $translator, Filesystem $fil * * @param string|null $locale optional filter to clear only one locale */ - public function clearAndWarmUp(?string $locale = null): void + public function clearAndWarmUp(string $locale = null): void { $translationDir = sprintf('%s/translations', $this->kernelCacheDir); diff --git a/Service/Importer.php b/Service/Importer.php index 75ca10ed..46436d36 100644 --- a/Service/Importer.php +++ b/Service/Importer.php @@ -62,10 +62,10 @@ public function __construct(Extractor $extractor, Environment $twig, string $def * @param MessageCatalogue[] $catalogues * @param array $config { * - * @var array $blacklist_domains Blacklist the domains we should exclude. Cannot be used with whitelist. - * @var array $whitelist_domains Whitelist the domains we should include. Cannot be used with blacklist. - * @var string $project_root The project root will be removed from the source location. - * } + * @var array $blacklist_domains Blacklist the domains we should exclude. Cannot be used with whitelist. + * @var array $whitelist_domains Whitelist the domains we should include. Cannot be used with blacklist. + * @var string $project_root The project root will be removed from the source location. + * } */ public function extractToCatalogues(Finder $finder, array $catalogues, array $config = []): ImportResult { diff --git a/Translator/FallbackTranslator.php b/Translator/FallbackTranslator.php index f49222c2..cee1d93a 100644 --- a/Translator/FallbackTranslator.php +++ b/Translator/FallbackTranslator.php @@ -61,9 +61,6 @@ public function __construct(string $defaultLocale, $symfonyTranslator, Translato $this->defaultLocale = $defaultLocale; } - /** - * {@inheritdoc} - */ public function trans($id, array $parameters = [], $domain = null, $locale = null): string { $id = (string) $id; @@ -87,9 +84,6 @@ public function trans($id, array $parameters = [], $domain = null, $locale = nul return $this->translateWithSubstitutedParameters($orgString, $locale, $parameters); } - /** - * {@inheritdoc} - */ public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null): string { $id = (string) $id; @@ -113,25 +107,16 @@ public function transChoice($id, $number, array $parameters = [], $domain = null return $this->translateWithSubstitutedParameters($orgString, $locale, $parameters); } - /** - * {@inheritdoc} - */ public function setLocale($locale): void { $this->symfonyTranslator->setLocale($locale); } - /** - * {@inheritdoc} - */ public function getLocale(): string { return $this->symfonyTranslator->getLocale(); } - /** - * {@inheritdoc} - */ public function getCatalogue($locale = null): MessageCatalogueInterface { return $this->symfonyTranslator->getCatalogue($locale); diff --git a/Twig/EditInPlaceExtension.php b/Twig/EditInPlaceExtension.php index 9a6ba66d..950a5334 100644 --- a/Twig/EditInPlaceExtension.php +++ b/Twig/EditInPlaceExtension.php @@ -37,9 +37,6 @@ public function __construct(TranslationExtension $extension, RequestStack $reque $this->activator = $activator; } - /** - * {@inheritdoc} - */ public function getFilters(): array { return [ @@ -58,9 +55,6 @@ public function isSafe($node): array return $this->activator->checkRequest($request) ? ['html'] : []; } - /** - * {@inheritdoc} - */ public function getName(): string { return self::class; diff --git a/Twig/TranslationExtension.php b/Twig/TranslationExtension.php index cf039b13..b97b0f05 100644 --- a/Twig/TranslationExtension.php +++ b/Twig/TranslationExtension.php @@ -71,7 +71,7 @@ public function getNodeVisitors(): array return $visitors; } - public function transchoiceWithDefault(string $message, string $defaultMessage, int $count, array $arguments = [], ?string $domain = null, ?string $locale = null): string + public function transchoiceWithDefault(string $message, string $defaultMessage, int $count, array $arguments = [], string $domain = null, string $locale = null): string { if (null === $domain) { $domain = 'messages'; @@ -84,21 +84,11 @@ public function transchoiceWithDefault(string $message, string $defaultMessage, return $this->translator->transChoice($message, $count, array_merge(['%count%' => $count], $arguments), $domain, $locale); } - /** - * @param mixed $v - * - * @return mixed - */ public function desc($v) { return $v; } - /** - * @param mixed $v - * - * @return mixed - */ public function meaning($v) { return $v; diff --git a/composer.json b/composer.json index 3041ebd7..5cd088c3 100644 --- a/composer.json +++ b/composer.json @@ -10,19 +10,20 @@ } ], "require": { - "php": "^7.2 || ^8.0", - "symfony/framework-bundle": "^4.4.20 || ^5.2.5 || ^6.0", - "symfony/validator": "^4.4.20 || ^5.2.5 || ^6.0", - "symfony/translation": "^4.4.20 || ^5.2.5 || ^6.0", - "symfony/twig-bundle": "^4.4.20 || ^5.2.4 || ^6.0", - "symfony/finder": "^4.4.20 || ^5.2.4 || ^6.0", - "symfony/intl": "^4.4.20 || ^5.2.4 || ^6.0", + "php": "^8.0", + "symfony/framework-bundle": "^5.3 || ^6.0", + "symfony/validator": "^5.3 || ^6.0", + "symfony/translation": "^5.3 || ^6.0", + "symfony/twig-bundle": "^5.3 || ^6.0", + "symfony/finder": "^5.3 || ^6.0", + "symfony/intl": "^5.3 || ^6.0", + "symfony/console": "^5.3 || ^6.0", "php-translation/symfony-storage": "^2.1", "php-translation/extractor": "^2.0", "nyholm/nsa": "^1.1", "twig/twig": "^2.14.4 || ^3.3", - "symfony/asset": "^4.4.20 || ^5.2.4 || ^6.0" + "symfony/asset": "^5.3 || ^6.0" }, "require-dev": { "symfony/phpunit-bridge": "^5.2 || ^6.0", @@ -31,10 +32,9 @@ "php-http/curl-client": "^1.7 || ^2.0", "php-http/message": "^1.11", "php-http/message-factory": "^1.0.2", - "symfony/console": "^4.4.20 || ^5.2.5 || ^6.0", - "symfony/twig-bridge": "^4.4.20 || ^5.2.5 || ^6.0", - "symfony/dependency-injection": "^4.4.20 || ^5.2.5 || ^6.0", - "symfony/web-profiler-bundle": "^4.4.20 || ^5.2.4 || ^6.0", + "symfony/twig-bridge": "^5.3 || ^6.0", + "symfony/dependency-injection": "^5.3 || ^6.0", + "symfony/web-profiler-bundle": "^5.3 || ^6.0", "matthiasnoback/symfony-dependency-injection-test": "^4.1", "matthiasnoback/symfony-config-test": "^4.1", "nyholm/psr7": "^1.1", From 1eb4d47c451eaae3efe2ca99b80b433cc4aeaf58 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Tue, 11 Jul 2023 19:58:52 +0200 Subject: [PATCH 216/234] Update Changelog.md (#492) Preparing 0.14.0 release --- Changelog.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Changelog.md b/Changelog.md index f17b726a..c08ff6f2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,16 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.14.0 + +### Fixed + +* Remove symfony console $defaultName deprecation by @axi in https://github.com/php-translation/symfony-bundle/pull/491 + +### Removed + +* Dropped PHP 7.2, 7.3, 7.4, and 8.0 support by @axi in https://github.com/php-translation/symfony-bundle/pull/491 + ## 0.13.0 ### Added From cdeaa4dbf48d39e3640c81bdd0d7eb1f7c51e3c1 Mon Sep 17 00:00:00 2001 From: Gordon Franke Date: Tue, 12 Sep 2023 11:31:30 +0200 Subject: [PATCH 217/234] bug: js error use own object extends HTMLElement (#493) * bug: js error use own object extends HTMLElement * refac: remove unnecessary phpdoc * refac: remove old sf 4 stuff --- EventListener/AutoAddMissingTranslations.php | 11 ----------- EventListener/EditInPlaceResponseListener.php | 8 -------- Resources/public/js/editInPlace.js | 5 ++++- 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/EventListener/AutoAddMissingTranslations.php b/EventListener/AutoAddMissingTranslations.php index 5cfcac66..844888e5 100644 --- a/EventListener/AutoAddMissingTranslations.php +++ b/EventListener/AutoAddMissingTranslations.php @@ -11,7 +11,6 @@ namespace Translation\Bundle\EventListener; -use Symfony\Component\HttpKernel\Event\PostResponseEvent; use Symfony\Component\HttpKernel\Event\TerminateEvent; use Symfony\Component\Translation\DataCollectorTranslator; use Translation\Bundle\Service\StorageService; @@ -32,9 +31,6 @@ final class AutoAddMissingTranslations */ private $storage; - /** - * @param DataCollectorTranslator $translator - */ public function __construct(StorageService $storage, DataCollectorTranslator $translator = null) { $this->dataCollector = $translator; @@ -56,10 +52,3 @@ public function onTerminate(TerminateEvent $event): void } } } - -// PostResponseEvent have been renamed into ResponseEvent in sf 4.3 -// @see https://github.com/symfony/symfony/blob/master/UPGRADE-4.3.md#httpkernel -// To be removed once sf ^4.3 become the minimum supported version. -if (!class_exists(TerminateEvent::class) && class_exists(PostResponseEvent::class)) { - class_alias(PostResponseEvent::class, TerminateEvent::class); -} diff --git a/EventListener/EditInPlaceResponseListener.php b/EventListener/EditInPlaceResponseListener.php index 9c296e08..37a6ea52 100644 --- a/EventListener/EditInPlaceResponseListener.php +++ b/EventListener/EditInPlaceResponseListener.php @@ -12,7 +12,6 @@ namespace Translation\Bundle\EventListener; use Symfony\Component\Asset\Packages; -use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Translation\Bundle\EditInPlace\ActivatorInterface; @@ -128,10 +127,3 @@ public function onKernelResponse(ResponseEvent $event): void $event->getResponse()->setContent($content); } } - -// FilterResponseEvent have been renamed into ResponseEvent in sf 4.3 -// @see https://github.com/symfony/symfony/blob/master/UPGRADE-4.3.md#httpkernel -// To be removed once sf ^4.3 become the minimum supported version. -if (!class_exists(ResponseEvent::class) && class_exists(FilterResponseEvent::class)) { - class_alias(FilterResponseEvent::class, ResponseEvent::class); -} diff --git a/Resources/public/js/editInPlace.js b/Resources/public/js/editInPlace.js index aa412d9f..cf9b52e0 100644 --- a/Resources/public/js/editInPlace.js +++ b/Resources/public/js/editInPlace.js @@ -8,7 +8,10 @@ */ (function () { if (typeof customElements.define !== "undefined") { - customElements.define("x-trans", HTMLElement); + // it is not possible to use HTMLElement directly + class XTrans extends HTMLElement {} + + customElements.define("x-trans", XTrans); return; } From 32db5ce0541cdaa2957f24b3c12a0ab1588770c7 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Tue, 12 Sep 2023 11:35:28 +0200 Subject: [PATCH 218/234] Update Changelog for 0.14.1 upcoming release --- Changelog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Changelog.md b/Changelog.md index c08ff6f2..7558ca6c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,12 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +## 0.14.1 + +### Fixed + +* bug: js error use own object extends HTMLElement by @gimler in https://github.com/php-translation/symfony-bundle/pull/493 + ## 0.14.0 ### Fixed From 28d9df3f548a94918d23cc698a2e0e2af89bc020 Mon Sep 17 00:00:00 2001 From: Tac Tacelosky Date: Mon, 18 Sep 2023 15:41:20 -0400 Subject: [PATCH 219/234] Update Readme.md (#494) drop $ so gitclip works --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index ca4190df..cb35de12 100644 --- a/Readme.md +++ b/Readme.md @@ -10,7 +10,7 @@ Install this bundle via Composer: ```bash -$ composer require php-translation/symfony-bundle +composer require php-translation/symfony-bundle ``` If you're using [Symfony Flex][symfony_flex] - you're done! Symfony Flex will create default From daf77cb9595189bb4a8e7883f519a87afac2c74a Mon Sep 17 00:00:00 2001 From: Matthias Althaus Date: Thu, 16 Nov 2023 15:13:31 +0100 Subject: [PATCH 220/234] Fixed render error in profiler/translation.html.twig (fixes #496) (#497) --- Resources/views/SymfonyProfiler/translation.html.twig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Resources/views/SymfonyProfiler/translation.html.twig b/Resources/views/SymfonyProfiler/translation.html.twig index 064fe5e7..c307bcd9 100644 --- a/Resources/views/SymfonyProfiler/translation.html.twig +++ b/Resources/views/SymfonyProfiler/translation.html.twig @@ -81,11 +81,11 @@
- {% if withCheckbox %} + {% if withCheckbox %} + - {% endif %} - {{ message.locale }} {{ message.domain }} {{ message.count }} {{ message.translation }} + {% apply spaceless %} Edit | From 0fee437fe67e774584824252c7bf7d0ec8c43842 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Mon, 25 Dec 2023 22:43:27 +0100 Subject: [PATCH 224/234] Use Composer v2 on CI (#503) * Use Composer v2 on CI * Use Composer v2 in one more place on CI --- .github/workflows/ci.yml | 2 +- .github/workflows/static.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba3cef4f..3497825f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: tools: flex - name: Download dependencies - uses: ramsey/composer-install@v1 + uses: ramsey/composer-install@v2 env: SYMFONY_REQUIRE: ${{ matrix.sf_version }} with: diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index d5d853a0..aea0ebaa 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -26,7 +26,7 @@ jobs: coverage: none - name: Download dependencies - uses: ramsey/composer-install@v1 + uses: ramsey/composer-install@v2 with: composer-options: --no-interaction --prefer-dist --optimize-autoloader From dc3d5d13cb48ff8177575a204a4211bd051bcacc Mon Sep 17 00:00:00 2001 From: Andrey Bolonin Date: Thu, 11 Apr 2024 13:56:25 +0300 Subject: [PATCH 225/234] Add PHP 8.3, 8.4 to CI (#505) * Add PHP 8.3, 8.4 to CI * Remove PHP 8.0 * Update ci.yml --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3497825f..9eece1fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,11 +10,11 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.0', '8.1', '8.2'] + php: ['8.1', '8.2', '8.3', '8.4'] strategy: [ 'highest' ] sf_version: [''] include: - - php: 8.0 + - php: 8.1 strategy: 'lowest' steps: From 2b8094428d6c86c780b7170a9a3e75083ce9bad9 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Thu, 11 Apr 2024 13:07:18 +0200 Subject: [PATCH 226/234] Apply PHP CS Fixer changes (#507) --- Command/CheckMissingCommand.php | 2 +- Controller/SymfonyProfilerController.php | 2 +- Controller/WebUIController.php | 2 +- EditInPlace/Activator.php | 2 +- EditInPlace/ActivatorInterface.php | 2 +- EventListener/AutoAddMissingTranslations.php | 2 +- Service/CacheClearer.php | 2 +- Tests/Unit/Translator/EditInPlaceTranslatorTest.php | 2 +- Twig/TranslationExtension.php | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Command/CheckMissingCommand.php b/Command/CheckMissingCommand.php index be95e47e..e4b7faae 100644 --- a/Command/CheckMissingCommand.php +++ b/Command/CheckMissingCommand.php @@ -136,7 +136,7 @@ private function countEmptyTranslations(MessageCatalogueInterface $catalogue): i foreach ($catalogue->getDomains() as $domain) { $emptyTranslations = array_filter( $catalogue->all($domain), - function (string $message = null): bool { + function (?string $message = null): bool { return null === $message || '' === $message; } ); diff --git a/Controller/SymfonyProfilerController.php b/Controller/SymfonyProfilerController.php index b17f8502..8461ed7e 100644 --- a/Controller/SymfonyProfilerController.php +++ b/Controller/SymfonyProfilerController.php @@ -95,7 +95,7 @@ public function syncAction(Request $request, string $token): Response } /** - * @return \Symfony\Component\HttpFoundation\RedirectResponse|Response + * @return RedirectResponse|Response */ public function syncAllAction(Request $request, string $token): Response { diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index 0604ba29..2ce6de07 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -76,7 +76,7 @@ public function __construct( /** * Show a dashboard for the configuration. */ - public function indexAction(string $configName = null): Response + public function indexAction(?string $configName = null): Response { if (!$this->isWebUIEnabled) { return new Response('You are not allowed here. Check your config.', Response::HTTP_BAD_REQUEST); diff --git a/EditInPlace/Activator.php b/EditInPlace/Activator.php index b5c38583..b327521e 100644 --- a/EditInPlace/Activator.php +++ b/EditInPlace/Activator.php @@ -81,7 +81,7 @@ public function deactivate(): void } } - public function checkRequest(Request $request = null): bool + public function checkRequest(?Request $request = null): bool { if (null === $this->getSession() || !$this->getSession()->has(self::KEY)) { return false; diff --git a/EditInPlace/ActivatorInterface.php b/EditInPlace/ActivatorInterface.php index 13c761fa..7100abd5 100644 --- a/EditInPlace/ActivatorInterface.php +++ b/EditInPlace/ActivatorInterface.php @@ -21,5 +21,5 @@ interface ActivatorInterface /** * Tells if the Edit In Place mode is enabled for this request. */ - public function checkRequest(Request $request = null): bool; + public function checkRequest(?Request $request = null): bool; } diff --git a/EventListener/AutoAddMissingTranslations.php b/EventListener/AutoAddMissingTranslations.php index 844888e5..195bf478 100644 --- a/EventListener/AutoAddMissingTranslations.php +++ b/EventListener/AutoAddMissingTranslations.php @@ -31,7 +31,7 @@ final class AutoAddMissingTranslations */ private $storage; - public function __construct(StorageService $storage, DataCollectorTranslator $translator = null) + public function __construct(StorageService $storage, ?DataCollectorTranslator $translator = null) { $this->dataCollector = $translator; $this->storage = $storage; diff --git a/Service/CacheClearer.php b/Service/CacheClearer.php index 6273a844..869ad637 100644 --- a/Service/CacheClearer.php +++ b/Service/CacheClearer.php @@ -58,7 +58,7 @@ public function __construct(string $kernelCacheDir, $translator, Filesystem $fil * * @param string|null $locale optional filter to clear only one locale */ - public function clearAndWarmUp(string $locale = null): void + public function clearAndWarmUp(?string $locale = null): void { $translationDir = sprintf('%s/translations', $this->kernelCacheDir); diff --git a/Tests/Unit/Translator/EditInPlaceTranslatorTest.php b/Tests/Unit/Translator/EditInPlaceTranslatorTest.php index 80e04004..04122189 100644 --- a/Tests/Unit/Translator/EditInPlaceTranslatorTest.php +++ b/Tests/Unit/Translator/EditInPlaceTranslatorTest.php @@ -113,7 +113,7 @@ public function __construct(bool $enabled = true) $this->enabled = $enabled; } - public function checkRequest(Request $request = null): bool + public function checkRequest(?Request $request = null): bool { return $this->enabled; } diff --git a/Twig/TranslationExtension.php b/Twig/TranslationExtension.php index b97b0f05..ba065e29 100644 --- a/Twig/TranslationExtension.php +++ b/Twig/TranslationExtension.php @@ -71,7 +71,7 @@ public function getNodeVisitors(): array return $visitors; } - public function transchoiceWithDefault(string $message, string $defaultMessage, int $count, array $arguments = [], string $domain = null, string $locale = null): string + public function transchoiceWithDefault(string $message, string $defaultMessage, int $count, array $arguments = [], ?string $domain = null, ?string $locale = null): string { if (null === $domain) { $domain = 'messages'; From 58e556c0897688b57d317ef10272f278d0d06f99 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Fri, 14 Jun 2024 10:13:19 +0200 Subject: [PATCH 227/234] Bump PHP to 8.1 in composer.json (#506) * Bump PHP to 8.1 in composer.json * Use PHP 8.1 on SA CI too * Rebuild CI * Fix PHPStan * Apply PHP CS Fixer changes --- .github/workflows/static.yml | 4 +-- Catalogue/CatalogueManager.php | 19 ++++++----- Command/DeleteEmptyCommand.php | 2 ++ Command/DeleteObsoleteCommand.php | 2 ++ Service/Importer.php | 10 +++--- composer.json | 5 +-- phpstan-baseline.neon | 56 ++++++++++++++++++------------- 7 files changed, 57 insertions(+), 41 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index aea0ebaa..ea6822ad 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -22,7 +22,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.0' + php-version: '8.1' coverage: none - name: Download dependencies @@ -63,7 +63,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.0' + php-version: '8.1' coverage: none - name: Download dependencies diff --git a/Catalogue/CatalogueManager.php b/Catalogue/CatalogueManager.php index 09a50d57..e9ce5aa0 100644 --- a/Catalogue/CatalogueManager.php +++ b/Catalogue/CatalogueManager.php @@ -67,26 +67,29 @@ public function getMessages(string $locale, string $domain): array /** * @param array $config { * - * @var string $domain - * @var string $locale - * @var bool $isNew - * @var bool $isObsolete - * @var bool $isApproved - * } - * - * @return CatalogueMessage[] + * @return CatalogueMessage[] Contains: + * - string $domain + * - string $locale + * - bool $isNew + * - bool $isObsolete + * - bool $isApproved */ public function findMessages(array $config = []): array { + /** @var string $inputDomain */ $inputDomain = $config['domain'] ?? null; + /** @var bool $isNew */ $isNew = $config['isNew'] ?? null; + /** @var bool $isObsolete */ $isObsolete = $config['isObsolete'] ?? null; + /** @var bool $isApproved */ $isApproved = $config['isApproved'] ?? null; $isEmpty = $config['isEmpty'] ?? null; $messages = []; $catalogues = []; if (isset($config['locale'])) { + /** @var string $locale */ $locale = $config['locale']; if (isset($this->catalogues[$locale])) { $catalogues = [$locale => $this->catalogues[$locale]]; diff --git a/Command/DeleteEmptyCommand.php b/Command/DeleteEmptyCommand.php index 75f290c9..dd4bc231 100644 --- a/Command/DeleteEmptyCommand.php +++ b/Command/DeleteEmptyCommand.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -96,6 +97,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($input->isInteractive()) { + /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); $question = new ConfirmationQuestion(sprintf('You are about to remove %d translations. Do you wish to continue? (y/N) ', $messageCount), false); if (!$helper->ask($input, $output, $question)) { diff --git a/Command/DeleteObsoleteCommand.php b/Command/DeleteObsoleteCommand.php index 74a04f73..c0ecc0c1 100644 --- a/Command/DeleteObsoleteCommand.php +++ b/Command/DeleteObsoleteCommand.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -97,6 +98,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($input->isInteractive()) { + /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); $question = new ConfirmationQuestion(sprintf('You are about to remove %d translations. Do you wish to continue? (y/N) ', $messageCount), false); if (!$helper->ask($input, $output, $question)) { diff --git a/Service/Importer.php b/Service/Importer.php index 46436d36..530ca8a7 100644 --- a/Service/Importer.php +++ b/Service/Importer.php @@ -60,12 +60,10 @@ public function __construct(Extractor $extractor, Environment $twig, string $def /** * @param MessageCatalogue[] $catalogues - * @param array $config { - * - * @var array $blacklist_domains Blacklist the domains we should exclude. Cannot be used with whitelist. - * @var array $whitelist_domains Whitelist the domains we should include. Cannot be used with blacklist. - * @var string $project_root The project root will be removed from the source location. - * } + * @param array $config Configuration options: + * - array $blacklist_domains Domains to be excluded. Cannot be used with whitelist. + * - array $whitelist_domains Domains to be included. Cannot be used with blacklist. + * - string $project_root The project root that will be removed from the source location. */ public function extractToCatalogues(Finder $finder, array $catalogues, array $config = []): ImportResult { diff --git a/composer.json b/composer.json index 5cd088c3..944f7aeb 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "symfony/framework-bundle": "^5.3 || ^6.0", "symfony/validator": "^5.3 || ^6.0", "symfony/translation": "^5.3 || ^6.0", @@ -38,7 +38,8 @@ "matthiasnoback/symfony-dependency-injection-test": "^4.1", "matthiasnoback/symfony-config-test": "^4.1", "nyholm/psr7": "^1.1", - "nyholm/symfony-bundle-test": "^2.0" + "nyholm/symfony-bundle-test": "^2.0", + "phpstan/phpstan": "^1.11" }, "suggest": { "php-http/httplug-bundle": "To easier configure your httplug clients." diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 30f38771..4f276f1a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -26,12 +26,12 @@ parameters: path: Command/StatusCommand.php - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\TreeBuilder\\:\\:root\\(\\)\\.$#" + message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeDefinition\\:\\:fixXmlConfig\\(\\)\\.$#" count: 1 path: DependencyInjection/Configuration.php - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeDefinition\\:\\:fixXmlConfig\\(\\)\\.$#" + message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\:\\:booleanNode\\(\\)\\.$#" count: 1 path: DependencyInjection/Configuration.php @@ -41,7 +41,7 @@ parameters: path: DependencyInjection/Configuration.php - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\:\\:booleanNode\\(\\)\\.$#" + message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\TreeBuilder\\:\\:root\\(\\)\\.$#" count: 1 path: DependencyInjection/Configuration.php @@ -51,22 +51,22 @@ parameters: path: Service/CacheClearer.php - - message: "#^Property Translation\\\\Bundle\\\\Translator\\\\EditInPlaceTranslator\\:\\:\\$translator has unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface as its type\\.$#" + message: "#^Call to method getCatalogue\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" count: 1 path: Translator/EditInPlaceTranslator.php - - message: "#^Parameter \\$translator of method Translation\\\\Bundle\\\\Translator\\\\EditInPlaceTranslator\\:\\:__construct\\(\\) has invalid typehint type Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" + message: "#^Call to method getCatalogues\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" count: 1 path: Translator/EditInPlaceTranslator.php - - message: "#^Class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface not found\\.$#" - count: 1 + message: "#^Call to method getLocale\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" + count: 2 path: Translator/EditInPlaceTranslator.php - - message: "#^Call to method getCatalogue\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" + message: "#^Call to method setLocale\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" count: 1 path: Translator/EditInPlaceTranslator.php @@ -76,32 +76,42 @@ parameters: path: Translator/EditInPlaceTranslator.php - - message: "#^Call to method getLocale\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" - count: 2 + message: "#^Call to method transChoice\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" + count: 1 path: Translator/EditInPlaceTranslator.php - - message: "#^Call to method transChoice\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" + message: "#^Class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface not found\\.$#" count: 1 path: Translator/EditInPlaceTranslator.php - - message: "#^Call to method setLocale\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" + message: "#^Parameter \\$translator of method Translation\\\\Bundle\\\\Translator\\\\EditInPlaceTranslator\\:\\:__construct\\(\\) has invalid type Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" count: 1 path: Translator/EditInPlaceTranslator.php - - message: "#^Property Translation\\\\Bundle\\\\Translator\\\\FallbackTranslator\\:\\:\\$symfonyTranslator has unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface as its type\\.$#" + message: "#^Property Translation\\\\Bundle\\\\Translator\\\\EditInPlaceTranslator\\:\\:\\$translator has unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface as its type\\.$#" + count: 1 + path: Translator/EditInPlaceTranslator.php + + - + message: "#^Call to method getCatalogue\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" count: 1 path: Translator/FallbackTranslator.php - - message: "#^Parameter \\$symfonyTranslator of method Translation\\\\Bundle\\\\Translator\\\\FallbackTranslator\\:\\:__construct\\(\\) has invalid typehint type Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" + message: "#^Call to method getCatalogues\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" count: 1 path: Translator/FallbackTranslator.php - - message: "#^Class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface not found\\.$#" + message: "#^Call to method getLocale\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" + count: 1 + path: Translator/FallbackTranslator.php + + - + message: "#^Call to method setLocale\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" count: 1 path: Translator/FallbackTranslator.php @@ -116,29 +126,29 @@ parameters: path: Translator/FallbackTranslator.php - - message: "#^Call to method setLocale\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" + message: "#^Class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface not found\\.$#" count: 1 path: Translator/FallbackTranslator.php - - message: "#^Call to method getLocale\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" + message: "#^Parameter \\$symfonyTranslator of method Translation\\\\Bundle\\\\Translator\\\\FallbackTranslator\\:\\:__construct\\(\\) has invalid type Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" count: 1 path: Translator/FallbackTranslator.php - - message: "#^Call to method getCatalogue\\(\\) on an unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface\\.$#" + message: "#^Property Translation\\\\Bundle\\\\Translator\\\\FallbackTranslator\\:\\:\\$symfonyTranslator has unknown class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface as its type\\.$#" count: 1 path: Translator/FallbackTranslator.php - - message: "#^Class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface not found\\.$#" - count: 1 + message: "#^Call to an undefined method Symfony\\\\Component\\\\Translation\\\\TranslatorBagInterface\\|Symfony\\\\Contracts\\\\Translation\\\\TranslatorInterface\\:\\:transChoice\\(\\)\\.$#" + count: 2 path: Twig/TranslationExtension.php - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Translation\\\\TranslatorBagInterface\\|Symfony\\\\Contracts\\\\Translation\\\\TranslatorInterface\\:\\:transChoice\\(\\)\\.$#" - count: 2 + message: "#^Class Symfony\\\\Component\\\\Translation\\\\TranslatorInterface not found\\.$#" + count: 1 path: Twig/TranslationExtension.php - excludes_analyse: + excludePaths: - Translator/TranslatorInterface.php From 0a8120dbc218dc1d106b6c57a56d82620effa3c1 Mon Sep 17 00:00:00 2001 From: Victor Bocharsky Date: Wed, 21 Aug 2024 12:50:40 +0200 Subject: [PATCH 228/234] Fix CS (#510) --- Command/CheckMissingCommand.php | 4 ++-- Command/DeleteEmptyCommand.php | 4 ++-- Command/DeleteObsoleteCommand.php | 4 ++-- Command/ExtractCommand.php | 2 +- Command/StorageTrait.php | 2 +- Command/SyncCommand.php | 2 +- Controller/SymfonyProfilerController.php | 6 +++--- Controller/WebUIController.php | 2 +- DependencyInjection/CompilerPass/StoragePass.php | 2 +- DependencyInjection/Configuration.php | 4 ++-- DependencyInjection/TranslationExtension.php | 2 +- EventListener/EditInPlaceResponseListener.php | 2 +- Service/CacheClearer.php | 2 +- Service/ConfigurationManager.php | 2 +- Service/Importer.php | 2 +- Service/StorageService.php | 2 +- Translator/EditInPlaceTranslator.php | 4 ++-- Translator/FallbackTranslator.php | 2 +- Twig/Node/Transchoice.php | 2 +- 19 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Command/CheckMissingCommand.php b/Command/CheckMissingCommand.php index e4b7faae..1325897c 100644 --- a/Command/CheckMissingCommand.php +++ b/Command/CheckMissingCommand.php @@ -93,7 +93,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io = new SymfonyStyle($input, $output); if ($newMessages > 0) { - $io->error(sprintf('%d new message(s) have been found, run bin/console translation:extract', $newMessages)); + $io->error(\sprintf('%d new message(s) have been found, run bin/console translation:extract', $newMessages)); return 1; } @@ -102,7 +102,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($emptyTranslations > 0) { $io->error( - sprintf('%d messages have empty translations, please provide translations for them', $emptyTranslations) + \sprintf('%d messages have empty translations, please provide translations for them', $emptyTranslations) ); return 1; diff --git a/Command/DeleteEmptyCommand.php b/Command/DeleteEmptyCommand.php index dd4bc231..c61523e6 100644 --- a/Command/DeleteEmptyCommand.php +++ b/Command/DeleteEmptyCommand.php @@ -99,7 +99,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($input->isInteractive()) { /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); - $question = new ConfirmationQuestion(sprintf('You are about to remove %d translations. Do you wish to continue? (y/N) ', $messageCount), false); + $question = new ConfirmationQuestion(\sprintf('You are about to remove %d translations. Do you wish to continue? (y/N) ', $messageCount), false); if (!$helper->ask($input, $output, $question)) { return 0; } @@ -112,7 +112,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int foreach ($messages as $message) { $storage->delete($message->getLocale(), $message->getDomain(), $message->getKey()); if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) { - $output->writeln(sprintf( + $output->writeln(\sprintf( 'Deleted empty message "%s" from domain "%s" and locale "%s"', $message->getKey(), $message->getDomain(), diff --git a/Command/DeleteObsoleteCommand.php b/Command/DeleteObsoleteCommand.php index c0ecc0c1..b3cd57a8 100644 --- a/Command/DeleteObsoleteCommand.php +++ b/Command/DeleteObsoleteCommand.php @@ -100,7 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($input->isInteractive()) { /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); - $question = new ConfirmationQuestion(sprintf('You are about to remove %d translations. Do you wish to continue? (y/N) ', $messageCount), false); + $question = new ConfirmationQuestion(\sprintf('You are about to remove %d translations. Do you wish to continue? (y/N) ', $messageCount), false); if (!$helper->ask($input, $output, $question)) { return 0; } @@ -113,7 +113,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int foreach ($messages as $message) { $storage->delete($message->getLocale(), $message->getDomain(), $message->getKey()); if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) { - $output->writeln(sprintf( + $output->writeln(\sprintf( 'Deleted obsolete message "%s" from domain "%s" and locale "%s"', $message->getKey(), $message->getDomain(), diff --git a/Command/ExtractCommand.php b/Command/ExtractCommand.php index 488b58fa..e7b3b858 100644 --- a/Command/ExtractCommand.php +++ b/Command/ExtractCommand.php @@ -130,7 +130,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** @var Error $error */ foreach ($errors as $error) { $io->error( - sprintf("%s\nLine: %s\nMessage: %s", $error->getPath(), $error->getLine(), $error->getMessage()) + \sprintf("%s\nLine: %s\nMessage: %s", $error->getPath(), $error->getLine(), $error->getMessage()) ); } } diff --git a/Command/StorageTrait.php b/Command/StorageTrait.php index fef48bbf..678e1603 100644 --- a/Command/StorageTrait.php +++ b/Command/StorageTrait.php @@ -31,7 +31,7 @@ private function getStorage($configName): StorageService if (null === $storage = $this->storageManager->getStorage($configName)) { $availableStorages = $this->storageManager->getNames(); - throw new \InvalidArgumentException(sprintf('Unknown storage "%s". Available storages are "%s".', $configName, implode('", "', $availableStorages))); + throw new \InvalidArgumentException(\sprintf('Unknown storage "%s". Available storages are "%s".', $configName, implode('", "', $availableStorages))); } return $storage; diff --git a/Command/SyncCommand.php b/Command/SyncCommand.php index 3da5af34..193da8a8 100644 --- a/Command/SyncCommand.php +++ b/Command/SyncCommand.php @@ -60,7 +60,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int break; default: - $output->writeln(sprintf('Direction must be either "up" or "down". Not "%s".', $input->getArgument('direction'))); + $output->writeln(\sprintf('Direction must be either "up" or "down". Not "%s".', $input->getArgument('direction'))); return 0; } diff --git a/Controller/SymfonyProfilerController.php b/Controller/SymfonyProfilerController.php index 8461ed7e..41a54e90 100644 --- a/Controller/SymfonyProfilerController.php +++ b/Controller/SymfonyProfilerController.php @@ -130,7 +130,7 @@ public function createAssetsAction(Request $request, string $token): Response $uploaded[] = $message; } - return new Response(sprintf('%s new assets created!', \count($uploaded))); + return new Response(\sprintf('%s new assets created!', \count($uploaded))); } private function getMessage(Request $request, string $token): SfProfilerMessage @@ -142,7 +142,7 @@ private function getMessage(Request $request, string $token): SfProfilerMessage $collectorMessages = $this->getMessages($token); if (!isset($collectorMessages[$messageId])) { - throw new NotFoundHttpException(sprintf('No message with key "%s" was found.', $messageId)); + throw new NotFoundHttpException(\sprintf('No message with key "%s" was found.', $messageId)); } $message = SfProfilerMessage::create($collectorMessages[$messageId]); @@ -152,7 +152,7 @@ private function getMessage(Request $request, string $token): SfProfilerMessage $message ->setLocale($requestCollector->getLocale()) - ->setTranslation(sprintf('[%s]', $message->getTranslation())) + ->setTranslation(\sprintf('[%s]', $message->getTranslation())) ; } diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index 2ce6de07..493ef64f 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -179,7 +179,7 @@ public function createAction(Request $request, string $configName, string $local try { $storage->create($message); } catch (StorageException $e) { - throw new BadRequestHttpException(sprintf('Key "%s" does already exist for "%s" on domain "%s".', $message->getKey(), $locale, $domain), $e); + throw new BadRequestHttpException(\sprintf('Key "%s" does already exist for "%s" on domain "%s".', $message->getKey(), $locale, $domain), $e); } catch (\Exception $e) { return new Response($e->getMessage(), Response::HTTP_BAD_REQUEST); } diff --git a/DependencyInjection/CompilerPass/StoragePass.php b/DependencyInjection/CompilerPass/StoragePass.php index 3e2b6034..ac448b6d 100644 --- a/DependencyInjection/CompilerPass/StoragePass.php +++ b/DependencyInjection/CompilerPass/StoragePass.php @@ -51,7 +51,7 @@ public function process(ContainerBuilder $container): void break; default: - throw new \LogicException(sprintf('The tag "php_translation.storage" must have a "type" of value "local" or "remote". Value "%s" was provided', $tag['type'])); + throw new \LogicException(\sprintf('The tag "php_translation.storage" must have a "type" of value "local" or "remote". Value "%s" was provided', $tag['type'])); } } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 95be8ca1..8fd7f064 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -117,7 +117,7 @@ private function configsNode(ArrayNodeDefinition $root): void $bundles = $container->getParameter('kernel.bundles'); if (!isset($bundles[$bundleName])) { - throw new \Exception(sprintf('The bundle "%s" does not exist. Available bundles: %s', $bundleName, array_keys($bundles))); + throw new \Exception(\sprintf('The bundle "%s" does not exist. Available bundles: %s', $bundleName, array_keys($bundles))); } $ref = new \ReflectionClass($bundles[$bundleName]); @@ -125,7 +125,7 @@ private function configsNode(ArrayNodeDefinition $root): void } if (!is_dir($value)) { - throw new \Exception(sprintf('The directory "%s" does not exist.', $value)); + throw new \Exception(\sprintf('The directory "%s" does not exist.', $value)); } return $value; diff --git a/DependencyInjection/TranslationExtension.php b/DependencyInjection/TranslationExtension.php index 2f1b1774..1ba8fede 100644 --- a/DependencyInjection/TranslationExtension.php +++ b/DependencyInjection/TranslationExtension.php @@ -182,7 +182,7 @@ private function enableEditInPlace(ContainerBuilder $container, array $config): $name = $config['edit_in_place']['config_name']; if ('default' !== $name && !isset($config['configs'][$name])) { - throw new InvalidArgumentException(sprintf('There is no config named "%s".', $name)); + throw new InvalidArgumentException(\sprintf('There is no config named "%s".', $name)); } $activatorRef = new Reference($config['edit_in_place']['activator']); diff --git a/EventListener/EditInPlaceResponseListener.php b/EventListener/EditInPlaceResponseListener.php index 37a6ea52..02583ee0 100644 --- a/EventListener/EditInPlaceResponseListener.php +++ b/EventListener/EditInPlaceResponseListener.php @@ -104,7 +104,7 @@ public function onKernelResponse(ResponseEvent $event): void } $content = preg_replace($pattern, $replacement, $content); - $html = sprintf( + $html = \sprintf( self::HTML, $this->packages->getUrl('bundles/translation/css/content-tools.min.css'), $this->packages->getUrl('bundles/translation/js/content-tools.min.js'), diff --git a/Service/CacheClearer.php b/Service/CacheClearer.php index 869ad637..9b5b44ef 100644 --- a/Service/CacheClearer.php +++ b/Service/CacheClearer.php @@ -60,7 +60,7 @@ public function __construct(string $kernelCacheDir, $translator, Filesystem $fil */ public function clearAndWarmUp(?string $locale = null): void { - $translationDir = sprintf('%s/translations', $this->kernelCacheDir); + $translationDir = \sprintf('%s/translations', $this->kernelCacheDir); $finder = new Finder(); diff --git a/Service/ConfigurationManager.php b/Service/ConfigurationManager.php index 5a689461..977ae135 100644 --- a/Service/ConfigurationManager.php +++ b/Service/ConfigurationManager.php @@ -50,7 +50,7 @@ public function getConfiguration($name = null): Configuration } } - throw new \InvalidArgumentException(sprintf('No configuration found for "%s"', $name)); + throw new \InvalidArgumentException(\sprintf('No configuration found for "%s"', $name)); } public function getFirstName(): ?string diff --git a/Service/Importer.php b/Service/Importer.php index 530ca8a7..ef0ec477 100644 --- a/Service/Importer.php +++ b/Service/Importer.php @@ -162,7 +162,7 @@ private function convertSourceLocationsToMessages( $trimLength = 1 + \strlen($this->config['project_root']); $meta = $this->getMetadata($catalogue, $key, $messageDomain); - $meta->addCategory('file-source', sprintf('%s:%s', substr($sourceLocation->getPath(), $trimLength), $sourceLocation->getLine())); + $meta->addCategory('file-source', \sprintf('%s:%s', substr($sourceLocation->getPath(), $trimLength), $sourceLocation->getLine())); if (isset($sourceLocation->getContext()['desc'])) { $meta->addCategory('desc', $sourceLocation->getContext()['desc']); } diff --git a/Service/StorageService.php b/Service/StorageService.php index 3dfc1dd3..88f797bb 100644 --- a/Service/StorageService.php +++ b/Service/StorageService.php @@ -87,7 +87,7 @@ public function sync(string $direction = self::DIRECTION_DOWN, array $importOpti break; default: - throw new LogicException(sprintf('Direction must be either "up" or "down". Value "%s" was provided', $direction)); + throw new LogicException(\sprintf('Direction must be either "up" or "down". Value "%s" was provided', $direction)); } } diff --git a/Translator/EditInPlaceTranslator.php b/Translator/EditInPlaceTranslator.php index 5c2c1dd8..6123a4c8 100644 --- a/Translator/EditInPlaceTranslator.php +++ b/Translator/EditInPlaceTranslator.php @@ -76,7 +76,7 @@ public function getCatalogue($locale = null): MessageCatalogueInterface public function getCatalogues(): array { if (!method_exists($this->translator, 'getCatalogues')) { - throw new \Exception(sprintf('%s method is not available! Please, upgrade to Symfony 6 in order to to use it', __METHOD__)); + throw new \Exception(\sprintf('%s method is not available! Please, upgrade to Symfony 6 in order to to use it', __METHOD__)); } return $this->translator->getCatalogues(); @@ -103,7 +103,7 @@ public function trans($id, array $parameters = [], $domain = null, $locale = nul } // Render all data in the translation tag required to allow in-line translation - return sprintf('%s', + return \sprintf('%s', $domain, $id, htmlspecialchars($original), diff --git a/Translator/FallbackTranslator.php b/Translator/FallbackTranslator.php index cee1d93a..57ffa223 100644 --- a/Translator/FallbackTranslator.php +++ b/Translator/FallbackTranslator.php @@ -125,7 +125,7 @@ public function getCatalogue($locale = null): MessageCatalogueInterface public function getCatalogues(): array { if (!method_exists($this->symfonyTranslator, 'getCatalogues')) { - throw new \Exception(sprintf('%s method is not available! Please, upgrade to Symfony 6 in order to to use it', __METHOD__)); + throw new \Exception(\sprintf('%s method is not available! Please, upgrade to Symfony 6 in order to to use it', __METHOD__)); } return $this->symfonyTranslator->getCatalogues(); diff --git a/Twig/Node/Transchoice.php b/Twig/Node/Transchoice.php index 1212a94d..d1e67753 100644 --- a/Twig/Node/Transchoice.php +++ b/Twig/Node/Transchoice.php @@ -25,7 +25,7 @@ public function __construct(ArrayExpression $arguments, $lineno) public function compile(Compiler $compiler): void { $compiler->raw( - sprintf( + \sprintf( '$this->env->getExtension(\'%s\')->%s(', 'Translation\Bundle\Twig\TranslationExtension', 'transchoiceWithDefault' From 0b3eaaa00794b29ef187bd5fa60b8d58a6d03330 Mon Sep 17 00:00:00 2001 From: Growiel Date: Thu, 22 Aug 2024 11:12:46 +0200 Subject: [PATCH 229/234] Support SF7 (#509) * update composer.json and add WebProfilerBundle to the test app * update dependencies to SF7 compatible versions --------- Co-authored-by: bocharsky-bw --- Tests/Functional/BaseTestCase.php | 2 ++ composer.json | 31 ++++++++++++++++--------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/Tests/Functional/BaseTestCase.php b/Tests/Functional/BaseTestCase.php index 84de6816..692d8340 100644 --- a/Tests/Functional/BaseTestCase.php +++ b/Tests/Functional/BaseTestCase.php @@ -15,6 +15,7 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\TwigBundle\TwigBundle; +use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle; use Symfony\Component\HttpKernel\Kernel; use Translation\Bundle\TranslationBundle; @@ -45,6 +46,7 @@ protected function setUp(): void $kernel->addTestBundle(TwigBundle::class); $kernel->addTestBundle(TranslationBundle::class); + $kernel->addTestBundle(WebProfilerBundle::class); $this->testKernel = $kernel; diff --git a/composer.json b/composer.json index 944f7aeb..e28adc8d 100644 --- a/composer.json +++ b/composer.json @@ -11,35 +11,36 @@ ], "require": { "php": "^8.1", - "symfony/framework-bundle": "^5.3 || ^6.0", - "symfony/validator": "^5.3 || ^6.0", - "symfony/translation": "^5.3 || ^6.0", - "symfony/twig-bundle": "^5.3 || ^6.0", - "symfony/finder": "^5.3 || ^6.0", - "symfony/intl": "^5.3 || ^6.0", - "symfony/console": "^5.3 || ^6.0", + "symfony/framework-bundle": "^5.3 || ^6.0 || ^7.0", + "symfony/validator": "^5.3 || ^6.0 || ^7.0", + "symfony/translation": "^5.3 || ^6.0 || ^7.0", + "symfony/twig-bundle": "^5.3 || ^6.0 || ^7.0", + "symfony/finder": "^5.3 || ^6.0 || ^7.0", + "symfony/intl": "^5.3 || ^6.0 || ^7.0", + "symfony/console": "^5.3 || ^6.0 || ^7.0", "php-translation/symfony-storage": "^2.1", "php-translation/extractor": "^2.0", "nyholm/nsa": "^1.1", "twig/twig": "^2.14.4 || ^3.3", - "symfony/asset": "^5.3 || ^6.0" + "symfony/asset": "^5.3 || ^6.0 || ^7.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.2 || ^6.0", + "symfony/phpunit-bridge": "^5.2 || ^6.0 || ^7.0", "bamarni/composer-bin-plugin": "^1.3", "php-translation/translator": "^1.0", "php-http/curl-client": "^1.7 || ^2.0", "php-http/message": "^1.11", "php-http/message-factory": "^1.0.2", - "symfony/twig-bridge": "^5.3 || ^6.0", - "symfony/dependency-injection": "^5.3 || ^6.0", - "symfony/web-profiler-bundle": "^5.3 || ^6.0", - "matthiasnoback/symfony-dependency-injection-test": "^4.1", - "matthiasnoback/symfony-config-test": "^4.1", + "symfony/twig-bridge": "^5.3 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.3 || ^6.0 || ^7.0", + "symfony/web-profiler-bundle": "^5.3 || ^6.0 || ^7.0", + "matthiasnoback/symfony-dependency-injection-test": "^5.1", + "matthiasnoback/symfony-config-test": "^5.2", "nyholm/psr7": "^1.1", "nyholm/symfony-bundle-test": "^2.0", - "phpstan/phpstan": "^1.11" + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^9.6" }, "suggest": { "php-http/httplug-bundle": "To easier configure your httplug clients." From c5ab0bc4c44fd98397e9515186d181fc7f514a65 Mon Sep 17 00:00:00 2001 From: Gordon Franke Date: Wed, 23 Oct 2024 13:42:39 +0200 Subject: [PATCH 230/234] refac: fix cs (#512) --- Command/CheckMissingCommand.php | 2 +- Command/DeleteEmptyCommand.php | 2 +- Command/DeleteObsoleteCommand.php | 2 +- Command/DownloadCommand.php | 2 +- Command/ExtractCommand.php | 2 +- Command/StatusCommand.php | 2 +- Controller/WebUIController.php | 2 +- Service/Importer.php | 2 +- Tests/Functional/app/Service/DummyMessageFactory.php | 4 ++-- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Command/CheckMissingCommand.php b/Command/CheckMissingCommand.php index 1325897c..7d00f46a 100644 --- a/Command/CheckMissingCommand.php +++ b/Command/CheckMissingCommand.php @@ -47,7 +47,7 @@ public function __construct( ConfigurationManager $configurationManager, CatalogueFetcher $catalogueFetcher, Importer $importer, - CatalogueCounter $catalogueCounter + CatalogueCounter $catalogueCounter, ) { parent::__construct(); diff --git a/Command/DeleteEmptyCommand.php b/Command/DeleteEmptyCommand.php index c61523e6..4eba260a 100644 --- a/Command/DeleteEmptyCommand.php +++ b/Command/DeleteEmptyCommand.php @@ -55,7 +55,7 @@ public function __construct( StorageManager $storageManager, ConfigurationManager $configurationManager, CatalogueManager $catalogueManager, - CatalogueFetcher $catalogueFetcher + CatalogueFetcher $catalogueFetcher, ) { $this->storageManager = $storageManager; $this->configurationManager = $configurationManager; diff --git a/Command/DeleteObsoleteCommand.php b/Command/DeleteObsoleteCommand.php index b3cd57a8..35e29a8d 100644 --- a/Command/DeleteObsoleteCommand.php +++ b/Command/DeleteObsoleteCommand.php @@ -55,7 +55,7 @@ public function __construct( StorageManager $storageManager, ConfigurationManager $configurationManager, CatalogueManager $catalogueManager, - CatalogueFetcher $catalogueFetcher + CatalogueFetcher $catalogueFetcher, ) { $this->storageManager = $storageManager; $this->configurationManager = $configurationManager; diff --git a/Command/DownloadCommand.php b/Command/DownloadCommand.php index f36d0535..8b7f6f1a 100644 --- a/Command/DownloadCommand.php +++ b/Command/DownloadCommand.php @@ -43,7 +43,7 @@ public function __construct( StorageManager $storageManager, ConfigurationManager $configurationManager, CacheClearer $cacheCleaner, - CatalogueWriter $catalogueWriter + CatalogueWriter $catalogueWriter, ) { $this->storageManager = $storageManager; $this->configurationManager = $configurationManager; diff --git a/Command/ExtractCommand.php b/Command/ExtractCommand.php index e7b3b858..d4d03cc3 100644 --- a/Command/ExtractCommand.php +++ b/Command/ExtractCommand.php @@ -67,7 +67,7 @@ public function __construct( CatalogueWriter $catalogueWriter, CatalogueCounter $catalogueCounter, Importer $importer, - ConfigurationManager $configurationManager + ConfigurationManager $configurationManager, ) { $this->catalogueFetcher = $catalogueFetcher; $this->catalogueWriter = $catalogueWriter; diff --git a/Command/StatusCommand.php b/Command/StatusCommand.php index 4c1a2bda..0f701b02 100644 --- a/Command/StatusCommand.php +++ b/Command/StatusCommand.php @@ -50,7 +50,7 @@ class StatusCommand extends Command public function __construct( CatalogueCounter $catalogueCounter, ConfigurationManager $configurationManager, - CatalogueFetcher $catalogueFetcher + CatalogueFetcher $catalogueFetcher, ) { $this->catalogueCounter = $catalogueCounter; $this->configurationManager = $configurationManager; diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index 493ef64f..aa0c40fa 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -58,7 +58,7 @@ public function __construct( bool $isWebUIEnabled, bool $isWebUIAllowCreate, bool $isWebUIAllowDelete, - string $fileBasePath + string $fileBasePath, ) { $this->configurationManager = $configurationManager; $this->catalogueFetcher = $catalogueFetcher; diff --git a/Service/Importer.php b/Service/Importer.php index ef0ec477..8213d533 100644 --- a/Service/Importer.php +++ b/Service/Importer.php @@ -131,7 +131,7 @@ public function extractToCatalogues(Finder $finder, array $catalogues, array $co private function convertSourceLocationsToMessages( MessageCatalogue $catalogue, SourceCollection $collection, - MessageCatalogue $currentCatalogue + MessageCatalogue $currentCatalogue, ): void { $currentMessages = NSA::getProperty($currentCatalogue, 'messages'); diff --git a/Tests/Functional/app/Service/DummyMessageFactory.php b/Tests/Functional/app/Service/DummyMessageFactory.php index d972edc8..381e71de 100644 --- a/Tests/Functional/app/Service/DummyMessageFactory.php +++ b/Tests/Functional/app/Service/DummyMessageFactory.php @@ -22,7 +22,7 @@ public function createRequest( $uri, array $headers = [], $body = null, - $protocolVersion = '1.1' + $protocolVersion = '1.1', ) { return new Request($method, $uri); } @@ -32,7 +32,7 @@ public function createResponse( $reasonPhrase = null, array $headers = [], $body = null, - $protocolVersion = '1.1' + $protocolVersion = '1.1', ) { return new Response(200); } From 73bfeae8322fac9a0d5d6dc0889d8dacb4778354 Mon Sep 17 00:00:00 2001 From: Gordon Franke Date: Thu, 27 Mar 2025 16:19:16 +0100 Subject: [PATCH 231/234] feat: add CI & static analysis badges (#517) --- .github/workflows/ci.yml | 3 +++ .github/workflows/static.yml | 7 +++++-- Readme.md | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9eece1fe..3e875581 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,10 @@ name: CI on: + push: + branches: [ "master" ] pull_request: + branches: [ "master" ] jobs: build: diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index ea6822ad..94029111 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -1,7 +1,10 @@ name: Static code analysis -on: [pull_request] - +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] jobs: phpstan: diff --git a/Readme.md b/Readme.md index cb35de12..389ec492 100644 --- a/Readme.md +++ b/Readme.md @@ -2,6 +2,8 @@ [![Latest Version](https://img.shields.io/github/release/php-translation/symfony-bundle.svg?style=flat-square)](https://github.com/php-translation/symfony-bundle/releases) [![Total Downloads](https://img.shields.io/packagist/dt/php-translation/symfony-bundle.svg?style=flat-square)](https://packagist.org/packages/php-translation/symfony-bundle) +[![CI](https://github.com/php-translation/symfony-bundle/actions/workflows/ci.yml/badge.svg)](https://github.com/php-translation/symfony-bundle/actions/workflows/ci.yml) +[![Static code analysis](https://github.com/php-translation/symfony-bundle/actions/workflows/static.yml/badge.svg)](https://github.com/php-translation/symfony-bundle/actions/workflows/static.yml) **Symfony integration for PHP Translation** From 553ea2bc07babe69505a94eddcc86639b7904014 Mon Sep 17 00:00:00 2001 From: Gordon Franke Date: Thu, 27 Mar 2025 16:24:24 +0100 Subject: [PATCH 232/234] github: fix static workflows update dependencies (#516) * github: fix static workflows update dependencies * refac: use ConditionalTernary when possible * refac: use Nodes instate of Node * refac: set operator attribute on EqualBinary expression * refac: migrate phpunit.dist.xml and fix max deprecation setting * feat: remove twig 2.x support no more maintained * refac: fix Symfony\Component\HttpKernel\DependencyInjection\Extension deprecation * refac: fix self deprecation mb_convert_encoding --- .github/workflows/static.yml | 18 +++--- DependencyInjection/TranslationExtension.php | 2 +- .../Functional/Controller/EditInPlaceTest.php | 4 +- Twig/Visitor/DefaultApplyingNodeVisitor.php | 53 +++++++++++++---- composer.json | 2 +- phpunit.xml.dist | 59 ++++++++----------- 6 files changed, 76 insertions(+), 62 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 94029111..f4b287db 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -9,14 +9,14 @@ on: jobs: phpstan: name: PHPStan - runs-on: Ubuntu-20.04 + runs-on: Ubuntu-22.04 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Cache PHPStan - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: .github/.cache/phpstan/ key: phpstan-${{ github.sha }} @@ -29,7 +29,7 @@ jobs: coverage: none - name: Download dependencies - uses: ramsey/composer-install@v2 + uses: ramsey/composer-install@v3 with: composer-options: --no-interaction --prefer-dist --optimize-autoloader @@ -43,7 +43,7 @@ jobs: name: PHP-CS-Fixer runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: PHP-CS-Fixer uses: docker://oskarstark/php-cs-fixer-ga with: @@ -51,13 +51,13 @@ jobs: psalm: name: Psalm - runs-on: Ubuntu-20.04 + runs-on: Ubuntu-22.04 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Cache Psalm - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: .github/.cache/psalm/ key: psalm-${{ github.sha }} @@ -70,7 +70,7 @@ jobs: coverage: none - name: Download dependencies - uses: ramsey/composer-install@v1 + uses: ramsey/composer-install@v3 with: composer-options: --no-interaction --prefer-dist --optimize-autoloader diff --git a/DependencyInjection/TranslationExtension.php b/DependencyInjection/TranslationExtension.php index 1ba8fede..ede6351c 100644 --- a/DependencyInjection/TranslationExtension.php +++ b/DependencyInjection/TranslationExtension.php @@ -16,9 +16,9 @@ use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\HttpKernel\Kernel; use Translation\Bundle\EventListener\AutoAddMissingTranslations; use Translation\Bundle\EventListener\EditInPlaceResponseListener; diff --git a/Tests/Functional/Controller/EditInPlaceTest.php b/Tests/Functional/Controller/EditInPlaceTest.php index 932af74e..733a1e28 100644 --- a/Tests/Functional/Controller/EditInPlaceTest.php +++ b/Tests/Functional/Controller/EditInPlaceTest.php @@ -40,7 +40,7 @@ public function testActivatedTest(): void self::assertStringContainsString('', $response->getContent()); $dom = new \DOMDocument('1.0', 'utf-8'); - @$dom->loadHTML(mb_convert_encoding($response->getContent(), 'HTML-ENTITIES', 'UTF-8')); + @$dom->loadHTML(mb_encode_numericentity($response->getContent(), [0x80, 0x10FFFF, 0, ~0], 'UTF-8')); $xpath = new \DOMXPath($dom); // Check number of x-trans tags @@ -79,7 +79,7 @@ public function testIfUntranslatableLabelGetsDisabled(): void self::assertStringContainsString('', $response->getContent()); $dom = new \DOMDocument('1.0', 'utf-8'); - @$dom->loadHTML(mb_convert_encoding($response->getContent(), 'HTML-ENTITIES', 'UTF-8')); + @$dom->loadHTML(mb_encode_numericentity($response->getContent(), [0x80, 0x10FFFF, 0, ~0], 'UTF-8')); $xpath = new \DOMXPath($dom); // Check number of x-trans tags diff --git a/Twig/Visitor/DefaultApplyingNodeVisitor.php b/Twig/Visitor/DefaultApplyingNodeVisitor.php index 9c112a7f..5be6d882 100644 --- a/Twig/Visitor/DefaultApplyingNodeVisitor.php +++ b/Twig/Visitor/DefaultApplyingNodeVisitor.php @@ -18,8 +18,11 @@ use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; +use Twig\Node\Expression\Ternary\ConditionalTernary; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\NodeVisitor\AbstractNodeVisitor; +use Twig\TwigFilter; /** * Applies the value of the "desc" filter if the "trans" filter has no @@ -93,22 +96,46 @@ public function doEnterNode(Node $node, Environment $env): Node $testNode->getNode('arguments')->setNode(0, new ArrayExpression([], $lineno)); // wrap the default node in a |replace filter - $defaultNode = new FilterExpression( - clone $node->getNode('arguments')->getNode(0), - new ConstantExpression('replace', $lineno), - new Node([ - clone $wrappingNode->getNode('arguments')->getNode(0), - ]), - $lineno + if (Environment::VERSION_ID >= 31500) { + $defaultNode = new FilterExpression( + clone $node->getNode('arguments')->getNode(0), + new TwigFilter('replace'), + new Nodes([ + clone $wrappingNode->getNode('arguments')->getNode(0), + ]), + $lineno + ); + } else { + $defaultNode = new FilterExpression( + clone $node->getNode('arguments')->getNode(0), + new ConstantExpression('replace', $lineno), + new Node([ + clone $wrappingNode->getNode('arguments')->getNode(0), + ]), + $lineno + ); + } + } + + $expr = new EqualBinary($testNode, $transNode->getNode('node'), $wrappingNode->getTemplateLine()); + if (Environment::VERSION_ID >= 31700) { + $expr->setAttribute('operator', 'binary_=='); + + $condition = new ConditionalTernary( + $expr, + $defaultNode, + clone $wrappingNode, + $wrappingNode->getTemplateLine() + ); + } else { + $condition = new ConditionalExpression( + $expr, + $defaultNode, + clone $wrappingNode, + $wrappingNode->getTemplateLine() ); } - $condition = new ConditionalExpression( - new EqualBinary($testNode, $transNode->getNode('node'), $wrappingNode->getTemplateLine()), - $defaultNode, - clone $wrappingNode, - $wrappingNode->getTemplateLine() - ); $node->setNode('node', $condition); return $node; diff --git a/composer.json b/composer.json index e28adc8d..3dd11d3a 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "php-translation/symfony-storage": "^2.1", "php-translation/extractor": "^2.0", "nyholm/nsa": "^1.1", - "twig/twig": "^2.14.4 || ^3.3", + "twig/twig": "^3.3", "symfony/asset": "^5.3 || ^6.0 || ^7.0" }, "require-dev": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0a4e1492..5e2a7616 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,38 +1,25 @@ - - - - - - - - - - - - - - - ./Tests - - - - - - ./ - - vendor - Tests - - - + + + + ./ + + + vendor + Tests + + + + + + + + + + + + + ./Tests + + From 8fd30daf52c14223fbe4cc11b8381b48d801e457 Mon Sep 17 00:00:00 2001 From: Gordon Franke Date: Wed, 16 Apr 2025 11:11:33 +0200 Subject: [PATCH 233/234] feat: use ubuntu-24.04 for github actions (#521) --- .github/workflows/ci.yml | 3 ++- .github/workflows/static.yml | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e875581..d64ab9a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,8 @@ on: jobs: build: name: Test - runs-on: Ubuntu-20.04 + runs-on: ubuntu-24.04 + strategy: fail-fast: false matrix: diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index f4b287db..474d282d 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -9,7 +9,7 @@ on: jobs: phpstan: name: PHPStan - runs-on: Ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout code @@ -41,7 +41,7 @@ jobs: php-cs-fixer: name: PHP-CS-Fixer - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: PHP-CS-Fixer @@ -51,7 +51,7 @@ jobs: psalm: name: Psalm - runs-on: Ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout code uses: actions/checkout@v4 From 589beee3e8489816a6f49c9b02e3e82c35d4a2d8 Mon Sep 17 00:00:00 2001 From: Gordon Franke Date: Tue, 22 Apr 2025 23:28:49 +0200 Subject: [PATCH 234/234] refac: replace AbstractNodeVisitor with NodeVisitorInterface (#520) --- Twig/Visitor/DefaultApplyingNodeVisitor.php | 13 +++++-------- Twig/Visitor/NormalizingNodeVisitor.php | 11 ++++------- Twig/Visitor/RemovingNodeVisitor.php | 13 +++++-------- 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/Twig/Visitor/DefaultApplyingNodeVisitor.php b/Twig/Visitor/DefaultApplyingNodeVisitor.php index 5be6d882..7d682c61 100644 --- a/Twig/Visitor/DefaultApplyingNodeVisitor.php +++ b/Twig/Visitor/DefaultApplyingNodeVisitor.php @@ -21,7 +21,7 @@ use Twig\Node\Expression\Ternary\ConditionalTernary; use Twig\Node\Node; use Twig\Node\Nodes; -use Twig\NodeVisitor\AbstractNodeVisitor; +use Twig\NodeVisitor\NodeVisitorInterface; use Twig\TwigFilter; /** @@ -32,19 +32,16 @@ * * @author Johannes M. Schmitt */ -final class DefaultApplyingNodeVisitor extends AbstractNodeVisitor +final class DefaultApplyingNodeVisitor implements NodeVisitorInterface { - /** - * @var bool - */ - private $enabled = true; + private bool $enabled = true; public function setEnabled(bool $bool): void { $this->enabled = $bool; } - public function doEnterNode(Node $node, Environment $env): Node + public function enterNode(Node $node, Environment $env): Node { if (!$this->enabled) { return $node; @@ -141,7 +138,7 @@ public function doEnterNode(Node $node, Environment $env): Node return $node; } - public function doLeaveNode(Node $node, Environment $env): Node + public function leaveNode(Node $node, Environment $env): Node { return $node; } diff --git a/Twig/Visitor/NormalizingNodeVisitor.php b/Twig/Visitor/NormalizingNodeVisitor.php index 11f8c640..045efdf3 100644 --- a/Twig/Visitor/NormalizingNodeVisitor.php +++ b/Twig/Visitor/NormalizingNodeVisitor.php @@ -15,7 +15,7 @@ use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Node; -use Twig\NodeVisitor\AbstractNodeVisitor; +use Twig\NodeVisitor\NodeVisitorInterface; /** * Performs equivalence transformations on the AST to ensure that @@ -25,17 +25,14 @@ * * @author Johannes M. Schmitt */ -final class NormalizingNodeVisitor extends AbstractNodeVisitor +final class NormalizingNodeVisitor implements NodeVisitorInterface { - protected function doEnterNode(Node $node, Environment $env): Node + public function enterNode(Node $node, Environment $env): Node { return $node; } - /** - * @return ConstantExpression|Node - */ - protected function doLeaveNode(Node $node, Environment $env): Node + public function leaveNode(Node $node, Environment $env): ConstantExpression|Node { if ($node instanceof ConcatBinary && ($left = $node->getNode('left')) instanceof ConstantExpression diff --git a/Twig/Visitor/RemovingNodeVisitor.php b/Twig/Visitor/RemovingNodeVisitor.php index daceacbb..195326cd 100644 --- a/Twig/Visitor/RemovingNodeVisitor.php +++ b/Twig/Visitor/RemovingNodeVisitor.php @@ -14,26 +14,23 @@ use Twig\Environment; use Twig\Node\Expression\FilterExpression; use Twig\Node\Node; -use Twig\NodeVisitor\AbstractNodeVisitor; +use Twig\NodeVisitor\NodeVisitorInterface; /** * Removes translation metadata filters from the AST. * * @author Johannes M. Schmitt */ -final class RemovingNodeVisitor extends AbstractNodeVisitor +final class RemovingNodeVisitor implements NodeVisitorInterface { - /** - * @var bool - */ - private $enabled = true; + private bool $enabled = true; public function setEnabled(bool $bool): void { $this->enabled = $bool; } - protected function doEnterNode(Node $node, Environment $env): Node + public function enterNode(Node $node, Environment $env): Node { if ($this->enabled && $node instanceof FilterExpression) { $name = $node->getNode('filter')->getAttribute('value'); @@ -46,7 +43,7 @@ protected function doEnterNode(Node $node, Environment $env): Node return $node; } - protected function doLeaveNode(Node $node, Environment $env): Node + public function leaveNode(Node $node, Environment $env): Node { return $node; }