diff --git a/Catalogue/CatalogueManager.php b/Catalogue/CatalogueManager.php index 3ffa039b..423011a6 100644 --- a/Catalogue/CatalogueManager.php +++ b/Catalogue/CatalogueManager.php @@ -12,7 +12,7 @@ namespace Translation\Bundle\Catalogue; use Symfony\Component\Translation\MessageCatalogue; -use Translation\Bundle\Model\Message; +use Translation\Bundle\Model\CatalogueMessage; /** * A manager that handle loaded catalogues. @@ -65,7 +65,7 @@ public function getDomains() * @param string $locale * @param string $domain * - * @return Message[] + * @return CatalogueMessage[] */ public function getMessages($locale, $domain) { @@ -75,7 +75,7 @@ public function getMessages($locale, $domain) } foreach ($this->catalogues[$locale]->all($domain) as $key => $text) { - $messages[] = new Message($this, $locale, $domain, $key, $text); + $messages[] = new CatalogueMessage($this, $locale, $domain, $key, $text); } return $messages; diff --git a/Controller/SymfonyProfilerController.php b/Controller/SymfonyProfilerController.php index 1b5bf84f..3dbd2900 100644 --- a/Controller/SymfonyProfilerController.php +++ b/Controller/SymfonyProfilerController.php @@ -11,13 +11,12 @@ namespace Translation\Bundle\Controller; -use Happyr\TranslationBundle\Model\Message; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Translation\DataCollectorTranslator; +use Translation\Bundle\Model\SfProfilerMessage; +use Translation\Common\Model\Message; /** * @author Tobias Nyholm @@ -28,13 +27,11 @@ class SymfonyProfilerController extends Controller * @param Request $request * @param string $token * - * @Route("/{token}/translation/edit", name="_profiler_translations_edit") - * * @return Response */ public function editAction(Request $request, $token) { - if (!$this->getParameter('translation.toolbar.allow_edit')) { + if (!$this->getParameter('php_translation.toolbar.allow_edit')) { return new Response('You are not allowed to edit the translations.'); } @@ -43,20 +40,20 @@ public function editAction(Request $request, $token) } $message = $this->getMessage($request, $token); - $trans = $this->get('happyr.translation'); + $storage = $this->get('php_translation.storage'); if ($request->isMethod('GET')) { - $trans->fetchTranslation($message); + $translation = $storage->syncAndFetchMessage($message->getLocale(), $message->getDomain(), $message->getKey()); - return $this->render('HappyrTranslationBundle:Profiler:edit.html.twig', [ - 'message' => $message, - 'key' => $request->query->get('message_id'), + return $this->render('TranslationBundle:SymfonyProfiler:edit.html.twig', [ + 'message' => $translation, + 'key' => $message->getLocale().$message->getDomain().$message->getKey(), ]); } //Assert: This is a POST request $message->setTranslation($request->request->get('translation')); - $trans->updateTranslation($message); + $storage->update($message->convertToMessage()); return new Response($message->getTranslation()); } @@ -65,9 +62,6 @@ public function editAction(Request $request, $token) * @param Request $request * @param string $token * - * @Route("/{token}/translation/flag", name="_profiler_translations_flag") - * @Method("POST") - * * @return Response */ public function flagAction(Request $request, $token) @@ -78,7 +72,8 @@ public function flagAction(Request $request, $token) $message = $this->getMessage($request, $token); - $saved = $this->get('happyr.translation')->flagTranslation($message); + // TODO + $saved = false; return new Response($saved ? 'OK' : 'ERROR'); } @@ -87,9 +82,6 @@ public function flagAction(Request $request, $token) * @param Request $request * @param string $token * - * @Route("/{token}/translation/sync", name="_profiler_translations_sync") - * @Method("POST") - * * @return Response */ public function syncAction(Request $request, $token) @@ -98,11 +90,11 @@ public function syncAction(Request $request, $token) return $this->redirectToRoute('_profiler', ['token' => $token]); } - $message = $this->getMessage($request, $token); - $translation = $this->get('happyr.translation')->fetchTranslation($message, true); + $sfMessage = $this->getMessage($request, $token); + $message = $this->get('php_translation.storage')->syncAndFetchMessage($sfMessage->getLocale(), $sfMessage->getDomain(), $sfMessage->getKey()); - if ($translation !== null) { - return new Response($translation); + if ($message !== null) { + return new Response($message->getTranslation()); } return new Response('Asset not found', 404); @@ -112,8 +104,6 @@ public function syncAction(Request $request, $token) * @param Request $request * @param $token * - * @Route("/{token}/translation/sync-all", name="_profiler_translations_sync_all") - * * @return \Symfony\Component\HttpFoundation\RedirectResponse|Response */ public function syncAllAction(Request $request, $token) @@ -122,7 +112,7 @@ public function syncAllAction(Request $request, $token) return $this->redirectToRoute('_profiler', ['token' => $token]); } - $this->get('happyr.translation')->synchronizeAllTranslations(); + $this->get('php_translation.storage')->sync(); return new Response('Started synchronization of all translations'); } @@ -135,9 +125,6 @@ public function syncAllAction(Request $request, $token) * @param Request $request * @param string $token * - * @Route("/{token}/translation/create-asset", name="_profiler_translations_create_assets") - * @Method("POST") - * * @return Response */ public function createAssetsAction(Request $request, $token) @@ -152,26 +139,21 @@ public function createAssetsAction(Request $request, $token) } $uploaded = []; - $trans = $this->get('happyr.translation'); + $trans = $this->get('php_translation.storage'); foreach ($messages as $message) { - if ($trans->createAsset($message)) { + if ($trans->update($message)) { $uploaded[] = $message; } } - $saved = count($uploaded); - if ($saved > 0) { - $this->get('happyr.translation.filesystem')->updateMessageCatalog($uploaded); - } - - return new Response(sprintf('%s new assets created!', $saved)); + return new Response(sprintf('%s new assets created!', count($uploaded))); } /** * @param Request $request * @param string $token * - * @return Message + * @return SfProfilerMessage */ protected function getMessage(Request $request, $token) { @@ -185,7 +167,7 @@ protected function getMessage(Request $request, $token) if (!isset($messages[$messageId])) { throw $this->createNotFoundException(sprintf('No message with key "%s" was found.', $messageId)); } - $message = new Message($messages[$messageId]); + $message = SfProfilerMessage::create($messages[$messageId]); if ($message->getState() === DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK) { $message->setLocale($profile->getCollector('request')->getLocale()) diff --git a/Controller/WebUIController.php b/Controller/WebUIController.php index e3490efa..7e8721ff 100644 --- a/Controller/WebUIController.php +++ b/Controller/WebUIController.php @@ -19,9 +19,9 @@ use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Translator; use Translation\Bundle\Exception\MessageValidationException; -use Translation\Bundle\Model\GuiMessageRepresentation; +use Translation\Bundle\Model\WebUiMessage; use Translation\Common\Exception\StorageException; -use Translation\Bundle\Model\Message; +use Translation\Bundle\Model\CatalogueMessage; /** * @author Tobias Nyholm @@ -87,9 +87,9 @@ public function showAction($configName, $locale, $domain) $catalogueManager = $this->get('php_translation.catalogue_manager'); $catalogueManager->load($catalogues); - /** @var Message[] $messages */ + /** @var CatalogueMessage[] $messages */ $messages = $catalogueManager->getMessages($locale, $domain); - usort($messages, function (Message $a, Message $b) { + usort($messages, function (CatalogueMessage $a, CatalogueMessage $b) { return strcmp($a->getKey(), $b->getKey()); }); @@ -201,13 +201,13 @@ private function getConfiguration(&$configName) * @param Request $request * @param array $validationGroups * - * @return GuiMessageRepresentation + * @return WebUiMessage */ private function getMessage(Request $request, array $validationGroups = []) { $json = $request->getContent(); $data = json_decode($json, true); - $message = new GuiMessageRepresentation(); + $message = new WebUiMessage(); if (isset($data['key'])) { $message->setKey($data['key']); } diff --git a/DependencyInjection/CompilerPass/StoragePass.php b/DependencyInjection/CompilerPass/StoragePass.php new file mode 100644 index 00000000..f8ae428c --- /dev/null +++ b/DependencyInjection/CompilerPass/StoragePass.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\DependencyInjection\CompilerPass; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Register all storages in the StorageService. + * + * @author Tobias Nyholm + */ +class StoragePass implements CompilerPassInterface +{ + /** + * @var Definition[] + */ + private $definitions; + + public function process(ContainerBuilder $container) + { + $services = $container->findTaggedServiceIds('php_translation.storage'); + foreach ($services as $id => $tags) { + foreach ($tags as $tag) { + if (!isset($tag['name'])) { + $tag['name'] = 'default'; + } + if (!isset($tag['type'])) { + throw new \LogicException('The tag "php_translation.storage" must have a "type".'); + } + + $def = $this->getDefinition($container, $tag['name']); + switch ($tag['type']) { + case 'remote': + $def->addMethodCall('addRemoteStorage', [new Reference($id)]); + break; + case 'local': + $def->addMethodCall('addLocalStorage', [new Reference($id)]); + 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'])); + } + } + } + } + + /** + * @param ContainerBuilder $container + * + * @return Definition + */ + private function getDefinition(ContainerBuilder $container, $name) + { + if (!isset($this->definitions[$name])) { + $this->definitions[$name] = $container->getDefinition('php_translation.storage.'.$name); + } + + return $this->definitions[$name]; + } +} diff --git a/DependencyInjection/TranslationExtension.php b/DependencyInjection/TranslationExtension.php index 28e0b837..353c1d9e 100644 --- a/DependencyInjection/TranslationExtension.php +++ b/DependencyInjection/TranslationExtension.php @@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\DependencyInjection\Loader; +use Translation\Bundle\Service\StorageService; /** * This is the class that loads and manages your bundle configuration. @@ -44,19 +45,38 @@ public function load(array $configs, ContainerBuilder $container) $this->enableWebUi($container, $config); } + if ($config['symfony_profiler']['enabled']) { + $loader->load('symfony_profiler.yml'); + $this->enableSymfonyProfiler($container, $config); + } + if ($config['fallback_translation']['enabled']) { $loader->load('auto_translation.yml'); $this->enableFallbackAutoTranslator($container, $config); } + $first = null; foreach ($config['configs'] as $name => &$c) { + if ($first === null || $name === 'default') { + $first = $name; + } if (empty($c['project_root'])) { $c['project_root'] = dirname($container->getParameter('kernel.root_dir')); } - $def = new DefinitionDecorator('php_translation.storage.file.abstract'); - $def->replaceArgument(2, $c['output_dir']); - $container->setDefinition('php_translation.storage.file.'.$name, $def); + $container->register('php_translation.storage.'.$name, StorageService::class); + + // Register a file storage + $def = new DefinitionDecorator('php_translation.single_storage.file.abstract'); + $def->replaceArgument(2, $c['output_dir']) + ->addTag('php_translation.storage', ['type' => 'local', 'name' => $name]); + $container->setDefinition('php_translation.single_storage.file.'.$name, $def); + } + + if ($first !== null) { + // Create some aliases for the default storage + $container->setAlias('php_translation.storage', 'php_translation.storage.'.$first); + $container->setAlias('php_translation.storage.default', 'php_translation.storage.'.$first); } $container->getDefinition('php_translation.configuration_manager') @@ -67,6 +87,11 @@ private function enableWebUi(ContainerBuilder $container, $config) { } + private function enableSymfonyProfiler(ContainerBuilder $container, $config) + { + $container->setParameter('php_translation.toolbar.allow_edit', $config['symfony_profiler']['allow_edit']); + } + private function enableFallbackAutoTranslator(ContainerBuilder $container, $config) { $externalTranslatorId = 'php_translation.translator_service.'.$config['fallback_translation']['service']; diff --git a/Model/Message.php b/Model/CatalogueMessage.php similarity index 94% rename from Model/Message.php rename to Model/CatalogueMessage.php index 511fca07..634c2e85 100644 --- a/Model/Message.php +++ b/Model/CatalogueMessage.php @@ -13,7 +13,12 @@ use Translation\Bundle\Catalogue\CatalogueManager; -class Message +/** + * A message representation for CatalogueManager. + * + * @author Tobias Nyholm + */ +class CatalogueMessage { /** * @var CatalogueManager diff --git a/Model/SfProfilerMessage.php b/Model/SfProfilerMessage.php new file mode 100644 index 00000000..3a8d0345 --- /dev/null +++ b/Model/SfProfilerMessage.php @@ -0,0 +1,301 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Model; + +use Translation\Common\Model\Message; + +/** + * @author Tobias Nyholm + */ +class SfProfilerMessage +{ + /** + * @var int + * + * This is the number of times the message occurs on a specific page + */ + private $count; + + /** + * @var string + * + * The domain the message belongs to + */ + private $domain; + + /** + * @var string + * + * The key/phrase you write in the source code + */ + private $key; + + /** + * @var string + * + * The locale the translations is on + */ + private $locale; + + /** + * @var int + * + * The current state of the translations. See Symfony\Component\Translation\DataCollectorTranslator + * + * MESSAGE_DEFINED = 0; + * MESSAGE_MISSING = 1; + * MESSAGE_EQUALS_FALLBACK = 2; + */ + private $state; + + /** + * @var string + * + * The translated string. This is the preview of the message. Ie no placeholders is visible. + */ + private $translation; + + /** + * @var int + * + * The number which we are feeding a transChoice with + * Used only in Symfony >2.8 + */ + private $transChoiceNumber; + + /** + * @var array + * + * The parameters sent to the translations + * Used only in Symfony >2.8 + */ + private $parameters; + + /** + * @param array $data + * + * @return SfProfilerMessage + */ + public static function create(array $data) + { + $message = new self(); + if (isset($data['id'])) { + $message->setKey($data['id']); + } + if (isset($data['domain'])) { + $message->setDomain($data['domain']); + } + if (isset($data['locale'])) { + $message->setLocale($data['locale']); + } + if (isset($data['translation'])) { + $message->setTranslation($data['translation']); + } + if (isset($data['state'])) { + $message->setState($data['state']); + } + if (isset($data['count'])) { + $message->setCount($data['count']); + } + if (isset($data['transChoiceNumber'])) { + $message->setTransChoiceNumber($data['transChoiceNumber']); + } + if (isset($data['parameters'])) { + $message->setParameters($data['parameters']); + } + + return $message; + } + + /** + * Convert to a Common\Message. + * + * @return Message + */ + public function convertToMessage() + { + return new Message( + $this->key, + $this->domain, + $this->locale, + $this->translation + ); + } + + /** + * @return int + */ + public function getCount() + { + return $this->count; + } + + /** + * @param int $count + * + * @return $this + */ + public function setCount($count) + { + $this->count = $count; + + return $this; + } + + /** + * @return string + */ + public function getDomain() + { + return $this->domain; + } + + /** + * @param string $domain + * + * @return $this + */ + public function setDomain($domain) + { + $this->domain = $domain; + + return $this; + } + + /** + * @return string + */ + public function getKey() + { + return $this->key; + } + + /** + * @param string $key + * + * @return $this + */ + public function setKey($key) + { + $this->key = $key; + + return $this; + } + + /** + * @return string + */ + public function getLocale() + { + return $this->locale; + } + + /** + * @param string $locale + * + * @return $this + */ + public function setLocale($locale) + { + $this->locale = $locale; + + return $this; + } + + /** + * @return int + */ + public function getState() + { + return $this->state; + } + + /** + * @param int $state + * + * @return $this + */ + public function setState($state) + { + $this->state = $state; + + return $this; + } + + /** + * @return string + */ + public function getTranslation() + { + return $this->translation; + } + + /** + * @param string $translation + * + * @return $this + */ + public function setTranslation($translation) + { + $this->translation = $translation; + + return $this; + } + + /** + * @return int + */ + public function getTransChoiceNumber() + { + return $this->transChoiceNumber; + } + + /** + * @param int $transChoiceNumber + * + * @return $this + */ + public function setTransChoiceNumber($transChoiceNumber) + { + $this->transChoiceNumber = $transChoiceNumber; + + return $this; + } + + /** + * @return array + */ + public function getParameters() + { + return $this->parameters; + } + + /** + * @return bool + */ + public function hasParameters() + { + return !empty($this->parameters); + } + + /** + * @param array $parameters + * + * @return $this + */ + public function setParameters($parameters) + { + $this->parameters = $parameters; + + return $this; + } +} diff --git a/Model/GuiMessageRepresentation.php b/Model/WebUiMessage.php similarity index 91% rename from Model/GuiMessageRepresentation.php rename to Model/WebUiMessage.php index 4fe3f884..939c7d43 100644 --- a/Model/GuiMessageRepresentation.php +++ b/Model/WebUiMessage.php @@ -16,7 +16,7 @@ /** * @author Tobias Nyholm */ -class GuiMessageRepresentation +class WebUiMessage { /** * @var string @@ -41,7 +41,7 @@ public function getKey() /** * @param string $key * - * @return GuiMessageRepresentation + * @return WebUiMessage */ public function setKey($key) { @@ -61,7 +61,7 @@ public function getMessage() /** * @param string $message * - * @return GuiMessageRepresentation + * @return WebUiMessage */ public function setMessage($message) { diff --git a/Resources/config/routing_symfony_profiler.yml b/Resources/config/routing_symfony_profiler.yml index e69de29b..255c9487 100644 --- a/Resources/config/routing_symfony_profiler.yml +++ b/Resources/config/routing_symfony_profiler.yml @@ -0,0 +1,25 @@ + +php_translation_profiler_translation_edit: + path: /{token}/translation/edit + methods: ["GET", "POST"] + defaults: { _controller: TranslationBundle:SymfonyProfiler:edit } + +php_translation_profiler_translation_flag: + path: /{token}/translation/flag + methods: ["POST"] + defaults: { _controller: TranslationBundle:SymfonyProfiler:flag } + +php_translation_profiler_translation_sync: + path: /{token}/translation/sync + methods: ["POST"] + defaults: { _controller: TranslationBundle:SymfonyProfiler:sync } + +php_translation_profiler_translation_sync_all: + path: /{token}/translation/sync_all + methods: ["POST"] + defaults: { _controller: TranslationBundle:SymfonyProfiler:syncAll } + +php_translation_profiler_translation_create_assets: + path: /{token}/translation/sync_all + methods: ["POST"] + defaults: { _controller: TranslationBundle:SymfonyProfiler:createAssets } diff --git a/Resources/config/services.yml b/Resources/config/services.yml index c6b3373c..12cb2688 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -18,7 +18,7 @@ services: class: Translation\Bundle\Service\Importer arguments: ["@php_translation.extractor"] - php_translation.storage.file.abstract: + php_translation.single_storage.file.abstract: class: Translation\Bundle\Storage\FileStorage abstract: true arguments: ["@translation.writer", "@translation.loader", ~] diff --git a/Resources/config/symfony_profiler.yml b/Resources/config/symfony_profiler.yml new file mode 100644 index 00000000..42affaa7 --- /dev/null +++ b/Resources/config/symfony_profiler.yml @@ -0,0 +1,6 @@ +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 } diff --git a/Resources/public/js/bootstrap4-alpha5.min.js b/Resources/public/js/bootstrap4-alpha5.min.js deleted file mode 100644 index 7759140c..00000000 --- a/Resources/public/js/bootstrap4-alpha5.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Bootstrap v4.0.0-alpha.5 (https://getbootstrap.com) - * Copyright 2011-2016 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ -if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(jQuery),+function(){function a(a,b){if(!a)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!b||"object"!=typeof b&&"function"!=typeof b?a:b}function b(a,b){if("function"!=typeof b&&null!==b)throw new TypeError("Super expression must either be null or a function, not "+typeof b);a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,enumerable:!1,writable:!0,configurable:!0}}),b&&(Object.setPrototypeOf?Object.setPrototypeOf(a,b):a.__proto__=b)}function c(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")}var d="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a},e=function(){function a(a,b){for(var c=0;cthis._items.length-1||b<0)){if(this._isSliding)return void a(this._element).one(r.SLID,function(){return c.to(b)});if(d===b)return this.pause(),void this.cycle();var e=b>d?q.NEXT:q.PREVIOUS;this._slide(e,this._items[b])}},j.prototype.dispose=function(){a(this._element).off(i),a.removeData(this._element,h),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},j.prototype._getConfig=function(c){return c=a.extend({},o,c),f.typeCheckConfig(b,c,p),c},j.prototype._addEventListeners=function(){this._config.keyboard&&a(this._element).on(r.KEYDOWN,a.proxy(this._keydown,this)),"hover"!==this._config.pause||"ontouchstart"in document.documentElement||a(this._element).on(r.MOUSEENTER,a.proxy(this.pause,this)).on(r.MOUSELEAVE,a.proxy(this.cycle,this))},j.prototype._keydown=function(a){if(a.preventDefault(),!/input|textarea/i.test(a.target.tagName))switch(a.which){case m:this.prev();break;case n:this.next();break;default:return}},j.prototype._getItemIndex=function(b){return this._items=a.makeArray(a(b).parent().find(t.ITEM)),this._items.indexOf(b)},j.prototype._getItemByDirection=function(a,b){var c=a===q.NEXT,d=a===q.PREVIOUS,e=this._getItemIndex(b),f=this._items.length-1,g=d&&0===e||c&&e===f;if(g&&!this._config.wrap)return b;var h=a===q.PREVIOUS?-1:1,i=(e+h)%this._items.length;return i===-1?this._items[this._items.length-1]:this._items[i]},j.prototype._triggerSlideEvent=function(b,c){var d=a.Event(r.SLIDE,{relatedTarget:b,direction:c});return a(this._element).trigger(d),d},j.prototype._setActiveIndicatorElement=function(b){if(this._indicatorsElement){a(this._indicatorsElement).find(t.ACTIVE).removeClass(s.ACTIVE);var c=this._indicatorsElement.children[this._getItemIndex(b)];c&&a(c).addClass(s.ACTIVE)}},j.prototype._slide=function(b,c){var d=this,e=a(this._element).find(t.ACTIVE_ITEM)[0],g=c||e&&this._getItemByDirection(b,e),h=Boolean(this._interval),i=b===q.NEXT?s.LEFT:s.RIGHT;if(g&&a(g).hasClass(s.ACTIVE))return void(this._isSliding=!1);var j=this._triggerSlideEvent(g,i);if(!j.isDefaultPrevented()&&e&&g){this._isSliding=!0,h&&this.pause(),this._setActiveIndicatorElement(g);var k=a.Event(r.SLID,{relatedTarget:g,direction:i});f.supportsTransitionEnd()&&a(this._element).hasClass(s.SLIDE)?(a(g).addClass(b),f.reflow(g),a(e).addClass(i),a(g).addClass(i),a(e).one(f.TRANSITION_END,function(){a(g).removeClass(i).removeClass(b),a(g).addClass(s.ACTIVE),a(e).removeClass(s.ACTIVE).removeClass(b).removeClass(i),d._isSliding=!1,setTimeout(function(){return a(d._element).trigger(k)},0)}).emulateTransitionEnd(l)):(a(e).removeClass(s.ACTIVE),a(g).addClass(s.ACTIVE),this._isSliding=!1,a(this._element).trigger(k)),h&&this.cycle()}},j._jQueryInterface=function(b){return this.each(function(){var c=a(this).data(h),e=a.extend({},o,a(this).data());"object"===("undefined"==typeof b?"undefined":d(b))&&a.extend(e,b);var f="string"==typeof b?b:e.slide;if(c||(c=new j(this,e),a(this).data(h,c)),"number"==typeof b)c.to(b);else if("string"==typeof f){if(void 0===c[f])throw new Error('No method named "'+f+'"');c[f]()}else e.interval&&(c.pause(),c.cycle())})},j._dataApiClickHandler=function(b){var c=f.getSelectorFromElement(this);if(c){var d=a(c)[0];if(d&&a(d).hasClass(s.CAROUSEL)){var e=a.extend({},a(d).data(),a(this).data()),g=this.getAttribute("data-slide-to");g&&(e.interval=!1),j._jQueryInterface.call(a(d),e),g&&a(d).data(h).to(g),b.preventDefault()}}},e(j,null,[{key:"VERSION",get:function(){return g}},{key:"Default",get:function(){return o}}]),j}();return a(document).on(r.CLICK_DATA_API,t.DATA_SLIDE,u._dataApiClickHandler),a(window).on(r.LOAD_DATA_API,function(){a(t.DATA_RIDE).each(function(){var b=a(this);u._jQueryInterface.call(b,b.data())})}),a.fn[b]=u._jQueryInterface,a.fn[b].Constructor=u,a.fn[b].noConflict=function(){return a.fn[b]=k,u._jQueryInterface},u}(jQuery),function(a){var b="collapse",g="4.0.0-alpha.5",h="bs.collapse",i="."+h,j=".data-api",k=a.fn[b],l=600,m={toggle:!0,parent:""},n={toggle:"boolean",parent:"string"},o={SHOW:"show"+i,SHOWN:"shown"+i,HIDE:"hide"+i,HIDDEN:"hidden"+i,CLICK_DATA_API:"click"+i+j},p={IN:"in",COLLAPSE:"collapse",COLLAPSING:"collapsing",COLLAPSED:"collapsed"},q={WIDTH:"width",HEIGHT:"height"},r={ACTIVES:".card > .in, .card > .collapsing",DATA_TOGGLE:'[data-toggle="collapse"]'},s=function(){function i(b,d){c(this,i),this._isTransitioning=!1,this._element=b,this._config=this._getConfig(d),this._triggerArray=a.makeArray(a('[data-toggle="collapse"][href="#'+b.id+'"],'+('[data-toggle="collapse"][data-target="#'+b.id+'"]'))),this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}return i.prototype.toggle=function(){a(this._element).hasClass(p.IN)?this.hide():this.show()},i.prototype.show=function(){var b=this;if(!this._isTransitioning&&!a(this._element).hasClass(p.IN)){var c=void 0,d=void 0;if(this._parent&&(c=a.makeArray(a(r.ACTIVES)),c.length||(c=null)),!(c&&(d=a(c).data(h),d&&d._isTransitioning))){var e=a.Event(o.SHOW);if(a(this._element).trigger(e),!e.isDefaultPrevented()){c&&(i._jQueryInterface.call(a(c),"hide"),d||a(c).data(h,null));var g=this._getDimension();a(this._element).removeClass(p.COLLAPSE).addClass(p.COLLAPSING),this._element.style[g]=0,this._element.setAttribute("aria-expanded",!0),this._triggerArray.length&&a(this._triggerArray).removeClass(p.COLLAPSED).attr("aria-expanded",!0),this.setTransitioning(!0);var j=function(){a(b._element).removeClass(p.COLLAPSING).addClass(p.COLLAPSE).addClass(p.IN),b._element.style[g]="",b.setTransitioning(!1),a(b._element).trigger(o.SHOWN)};if(!f.supportsTransitionEnd())return void j();var k=g[0].toUpperCase()+g.slice(1),m="scroll"+k;a(this._element).one(f.TRANSITION_END,j).emulateTransitionEnd(l),this._element.style[g]=this._element[m]+"px"}}}},i.prototype.hide=function(){var b=this;if(!this._isTransitioning&&a(this._element).hasClass(p.IN)){var c=a.Event(o.HIDE);if(a(this._element).trigger(c),!c.isDefaultPrevented()){var d=this._getDimension(),e=d===q.WIDTH?"offsetWidth":"offsetHeight";this._element.style[d]=this._element[e]+"px",f.reflow(this._element),a(this._element).addClass(p.COLLAPSING).removeClass(p.COLLAPSE).removeClass(p.IN),this._element.setAttribute("aria-expanded",!1),this._triggerArray.length&&a(this._triggerArray).addClass(p.COLLAPSED).attr("aria-expanded",!1),this.setTransitioning(!0);var g=function(){b.setTransitioning(!1),a(b._element).removeClass(p.COLLAPSING).addClass(p.COLLAPSE).trigger(o.HIDDEN)};return this._element.style[d]="",f.supportsTransitionEnd()?void a(this._element).one(f.TRANSITION_END,g).emulateTransitionEnd(l):void g()}}},i.prototype.setTransitioning=function(a){this._isTransitioning=a},i.prototype.dispose=function(){a.removeData(this._element,h),this._config=null,this._parent=null,this._element=null,this._triggerArray=null,this._isTransitioning=null},i.prototype._getConfig=function(c){return c=a.extend({},m,c),c.toggle=Boolean(c.toggle),f.typeCheckConfig(b,c,n),c},i.prototype._getDimension=function(){var b=a(this._element).hasClass(q.WIDTH);return b?q.WIDTH:q.HEIGHT},i.prototype._getParent=function(){var b=this,c=a(this._config.parent)[0],d='[data-toggle="collapse"][data-parent="'+this._config.parent+'"]';return a(c).find(d).each(function(a,c){b._addAriaAndCollapsedClass(i._getTargetFromElement(c),[c])}),c},i.prototype._addAriaAndCollapsedClass=function(b,c){if(b){var d=a(b).hasClass(p.IN);b.setAttribute("aria-expanded",d),c.length&&a(c).toggleClass(p.COLLAPSED,!d).attr("aria-expanded",d)}},i._getTargetFromElement=function(b){var c=f.getSelectorFromElement(b);return c?a(c)[0]:null},i._jQueryInterface=function(b){return this.each(function(){var c=a(this),e=c.data(h),f=a.extend({},m,c.data(),"object"===("undefined"==typeof b?"undefined":d(b))&&b);if(!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||(e=new i(this,f),c.data(h,e)),"string"==typeof b){if(void 0===e[b])throw new Error('No method named "'+b+'"');e[b]()}})},e(i,null,[{key:"VERSION",get:function(){return g}},{key:"Default",get:function(){return m}}]),i}();return a(document).on(o.CLICK_DATA_API,r.DATA_TOGGLE,function(b){b.preventDefault();var c=s._getTargetFromElement(this),d=a(c).data(h),e=d?"toggle":a(this).data();s._jQueryInterface.call(a(c),e)}),a.fn[b]=s._jQueryInterface,a.fn[b].Constructor=s,a.fn[b].noConflict=function(){return a.fn[b]=k,s._jQueryInterface},s}(jQuery),function(a){var b="dropdown",d="4.0.0-alpha.5",g="bs.dropdown",h="."+g,i=".data-api",j=a.fn[b],k=27,l=38,m=40,n=3,o={HIDE:"hide"+h,HIDDEN:"hidden"+h,SHOW:"show"+h,SHOWN:"shown"+h,CLICK:"click"+h,CLICK_DATA_API:"click"+h+i,KEYDOWN_DATA_API:"keydown"+h+i},p={BACKDROP:"dropdown-backdrop",DISABLED:"disabled",OPEN:"open"},q={BACKDROP:".dropdown-backdrop",DATA_TOGGLE:'[data-toggle="dropdown"]',FORM_CHILD:".dropdown form",ROLE_MENU:'[role="menu"]',ROLE_LISTBOX:'[role="listbox"]',NAVBAR_NAV:".navbar-nav",VISIBLE_ITEMS:'[role="menu"] li:not(.disabled) a, [role="listbox"] li:not(.disabled) a'},r=function(){function b(a){c(this,b),this._element=a,this._addEventListeners()}return b.prototype.toggle=function(){if(this.disabled||a(this).hasClass(p.DISABLED))return!1;var c=b._getParentFromElement(this),d=a(c).hasClass(p.OPEN);if(b._clearMenus(),d)return!1;if("ontouchstart"in document.documentElement&&!a(c).closest(q.NAVBAR_NAV).length){var e=document.createElement("div");e.className=p.BACKDROP,a(e).insertBefore(this),a(e).on("click",b._clearMenus)}var f={relatedTarget:this},g=a.Event(o.SHOW,f);return a(c).trigger(g),!g.isDefaultPrevented()&&(this.focus(),this.setAttribute("aria-expanded","true"),a(c).toggleClass(p.OPEN),a(c).trigger(a.Event(o.SHOWN,f)),!1)},b.prototype.dispose=function(){a.removeData(this._element,g),a(this._element).off(h),this._element=null},b.prototype._addEventListeners=function(){a(this._element).on(o.CLICK,this.toggle)},b._jQueryInterface=function(c){return this.each(function(){var d=a(this).data(g);if(d||a(this).data(g,d=new b(this)),"string"==typeof c){if(void 0===d[c])throw new Error('No method named "'+c+'"');d[c].call(this)}})},b._clearMenus=function(c){if(!c||c.which!==n){var d=a(q.BACKDROP)[0];d&&d.parentNode.removeChild(d);for(var e=a.makeArray(a(q.DATA_TOGGLE)),f=0;f0&&h--,c.which===m&&hdocument.documentElement.clientHeight;!this._isBodyOverflowing&&a&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!a&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},j.prototype._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},j.prototype._checkScrollbar=function(){this._isBodyOverflowing=document.body.clientWidth=c){var d=this._targets[this._targets.length-1];this._activeTarget!==d&&this._activate(d)}if(this._activeTarget&&a=this._offsets[e]&&(void 0===this._offsets[e+1]||a .nav-item .fade, > .fade",ACTIVE:".active",ACTIVE_CHILD:"> .nav-item > .active, > .active",DATA_TOGGLE:'[data-toggle="tab"], [data-toggle="pill"]', -DROPDOWN_TOGGLE:".dropdown-toggle",DROPDOWN_ACTIVE_CHILD:"> .dropdown-menu .active"},o=function(){function b(a){c(this,b),this._element=a}return b.prototype.show=function(){var b=this;if(!this._element.parentNode||this._element.parentNode.nodeType!==Node.ELEMENT_NODE||!a(this._element).hasClass(m.ACTIVE)){var c=void 0,d=void 0,e=a(this._element).closest(n.UL)[0],g=f.getSelectorFromElement(this._element);e&&(d=a.makeArray(a(e).find(n.ACTIVE)),d=d[d.length-1]);var h=a.Event(l.HIDE,{relatedTarget:this._element}),i=a.Event(l.SHOW,{relatedTarget:d});if(d&&a(d).trigger(h),a(this._element).trigger(i),!i.isDefaultPrevented()&&!h.isDefaultPrevented()){g&&(c=a(g)[0]),this._activate(this._element,e);var j=function(){var c=a.Event(l.HIDDEN,{relatedTarget:b._element}),e=a.Event(l.SHOWN,{relatedTarget:d});a(d).trigger(c),a(b._element).trigger(e)};c?this._activate(c,c.parentNode,j):j()}}},b.prototype.dispose=function(){a.removeClass(this._element,g),this._element=null},b.prototype._activate=function(b,c,d){var e=a(c).find(n.ACTIVE_CHILD)[0],g=d&&f.supportsTransitionEnd()&&(e&&a(e).hasClass(m.FADE)||Boolean(a(c).find(n.FADE_CHILD)[0])),h=a.proxy(this._transitionComplete,this,b,e,g,d);e&&g?a(e).one(f.TRANSITION_END,h).emulateTransitionEnd(k):h(),e&&a(e).removeClass(m.IN)},b.prototype._transitionComplete=function(b,c,d,e){if(c){a(c).removeClass(m.ACTIVE);var g=a(c).find(n.DROPDOWN_ACTIVE_CHILD)[0];g&&a(g).removeClass(m.ACTIVE),c.setAttribute("aria-expanded",!1)}if(a(b).addClass(m.ACTIVE),b.setAttribute("aria-expanded",!0),d?(f.reflow(b),a(b).addClass(m.IN)):a(b).removeClass(m.FADE),b.parentNode&&a(b.parentNode).hasClass(m.DROPDOWN_MENU)){var h=a(b).closest(n.DROPDOWN)[0];h&&a(h).find(n.DROPDOWN_TOGGLE).addClass(m.ACTIVE),b.setAttribute("aria-expanded",!0)}e&&e()},b._jQueryInterface=function(c){return this.each(function(){var d=a(this),e=d.data(g);if(e||(e=e=new b(this),d.data(g,e)),"string"==typeof c){if(void 0===e[c])throw new Error('No method named "'+c+'"');e[c]()}})},e(b,null,[{key:"VERSION",get:function(){return d}}]),b}();return a(document).on(l.CLICK_DATA_API,n.DATA_TOGGLE,function(b){b.preventDefault(),o._jQueryInterface.call(a(this),"show")}),a.fn[b]=o._jQueryInterface,a.fn[b].Constructor=o,a.fn[b].noConflict=function(){return a.fn[b]=j,o._jQueryInterface},o}(jQuery),function(a){if(void 0===window.Tether)throw new Error("Bootstrap tooltips require Tether (http://tether.io/)");var b="tooltip",g="4.0.0-alpha.5",h="bs.tooltip",i="."+h,j=a.fn[b],k=150,l="bs-tether",m={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:"0 0",constraints:[]},n={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"string",constraints:"array"},o={TOP:"bottom center",RIGHT:"middle left",BOTTOM:"top center",LEFT:"middle right"},p={IN:"in",OUT:"out"},q={HIDE:"hide"+i,HIDDEN:"hidden"+i,SHOW:"show"+i,SHOWN:"shown"+i,INSERTED:"inserted"+i,CLICK:"click"+i,FOCUSIN:"focusin"+i,FOCUSOUT:"focusout"+i,MOUSEENTER:"mouseenter"+i,MOUSELEAVE:"mouseleave"+i},r={FADE:"fade",IN:"in"},s={TOOLTIP:".tooltip",TOOLTIP_INNER:".tooltip-inner"},t={element:!1,enabled:!1},u={HOVER:"hover",FOCUS:"focus",CLICK:"click",MANUAL:"manual"},v=function(){function j(a,b){c(this,j),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._tether=null,this.element=a,this.config=this._getConfig(b),this.tip=null,this._setListeners()}return j.prototype.enable=function(){this._isEnabled=!0},j.prototype.disable=function(){this._isEnabled=!1},j.prototype.toggleEnabled=function(){this._isEnabled=!this._isEnabled},j.prototype.toggle=function(b){if(b){var c=this.constructor.DATA_KEY,d=a(b.currentTarget).data(c);d||(d=new this.constructor(b.currentTarget,this._getDelegateConfig()),a(b.currentTarget).data(c,d)),d._activeTrigger.click=!d._activeTrigger.click,d._isWithActiveTrigger()?d._enter(null,d):d._leave(null,d)}else{if(a(this.getTipElement()).hasClass(r.IN))return void this._leave(null,this);this._enter(null,this)}},j.prototype.dispose=function(){clearTimeout(this._timeout),this.cleanupTether(),a.removeData(this.element,this.constructor.DATA_KEY),a(this.element).off(this.constructor.EVENT_KEY),this.tip&&a(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._tether=null,this.element=null,this.config=null,this.tip=null},j.prototype.show=function(){var b=this,c=a.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){a(this.element).trigger(c);var d=a.contains(this.element.ownerDocument.documentElement,this.element);if(c.isDefaultPrevented()||!d)return;var e=this.getTipElement(),g=f.getUID(this.constructor.NAME);e.setAttribute("id",g),this.element.setAttribute("aria-describedby",g),this.setContent(),this.config.animation&&a(e).addClass(r.FADE);var h="function"==typeof this.config.placement?this.config.placement.call(this,e,this.element):this.config.placement,i=this._getAttachment(h);a(e).data(this.constructor.DATA_KEY,this).appendTo(document.body),a(this.element).trigger(this.constructor.Event.INSERTED),this._tether=new Tether({attachment:i,element:e,target:this.element,classes:t,classPrefix:l,offset:this.config.offset,constraints:this.config.constraints,addTargetClasses:!1}),f.reflow(e),this._tether.position(),a(e).addClass(r.IN);var k=function(){var c=b._hoverState;b._hoverState=null,a(b.element).trigger(b.constructor.Event.SHOWN),c===p.OUT&&b._leave(null,b)};if(f.supportsTransitionEnd()&&a(this.tip).hasClass(r.FADE))return void a(this.tip).one(f.TRANSITION_END,k).emulateTransitionEnd(j._TRANSITION_DURATION);k()}},j.prototype.hide=function(b){var c=this,d=this.getTipElement(),e=a.Event(this.constructor.Event.HIDE),g=function(){c._hoverState!==p.IN&&d.parentNode&&d.parentNode.removeChild(d),c.element.removeAttribute("aria-describedby"),a(c.element).trigger(c.constructor.Event.HIDDEN),c.cleanupTether(),b&&b()};a(this.element).trigger(e),e.isDefaultPrevented()||(a(d).removeClass(r.IN),f.supportsTransitionEnd()&&a(this.tip).hasClass(r.FADE)?a(d).one(f.TRANSITION_END,g).emulateTransitionEnd(k):g(),this._hoverState="")},j.prototype.isWithContent=function(){return Boolean(this.getTitle())},j.prototype.getTipElement=function(){return this.tip=this.tip||a(this.config.template)[0]},j.prototype.setContent=function(){var b=a(this.getTipElement());this.setElementContent(b.find(s.TOOLTIP_INNER),this.getTitle()),b.removeClass(r.FADE).removeClass(r.IN),this.cleanupTether()},j.prototype.setElementContent=function(b,c){var e=this.config.html;"object"===("undefined"==typeof c?"undefined":d(c))&&(c.nodeType||c.jquery)?e?a(c).parent().is(b)||b.empty().append(c):b.text(a(c).text()):b[e?"html":"text"](c)},j.prototype.getTitle=function(){var a=this.element.getAttribute("data-original-title");return a||(a="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),a},j.prototype.cleanupTether=function(){this._tether&&this._tether.destroy()},j.prototype._getAttachment=function(a){return o[a.toUpperCase()]},j.prototype._setListeners=function(){var b=this,c=this.config.trigger.split(" ");c.forEach(function(c){if("click"===c)a(b.element).on(b.constructor.Event.CLICK,b.config.selector,a.proxy(b.toggle,b));else if(c!==u.MANUAL){var d=c===u.HOVER?b.constructor.Event.MOUSEENTER:b.constructor.Event.FOCUSIN,e=c===u.HOVER?b.constructor.Event.MOUSELEAVE:b.constructor.Event.FOCUSOUT;a(b.element).on(d,b.config.selector,a.proxy(b._enter,b)).on(e,b.config.selector,a.proxy(b._leave,b))}}),this.config.selector?this.config=a.extend({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},j.prototype._fixTitle=function(){var a=d(this.element.getAttribute("data-original-title"));(this.element.getAttribute("title")||"string"!==a)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},j.prototype._enter=function(b,c){var d=this.constructor.DATA_KEY;return c=c||a(b.currentTarget).data(d),c||(c=new this.constructor(b.currentTarget,this._getDelegateConfig()),a(b.currentTarget).data(d,c)),b&&(c._activeTrigger["focusin"===b.type?u.FOCUS:u.HOVER]=!0),a(c.getTipElement()).hasClass(r.IN)||c._hoverState===p.IN?void(c._hoverState=p.IN):(clearTimeout(c._timeout),c._hoverState=p.IN,c.config.delay&&c.config.delay.show?void(c._timeout=setTimeout(function(){c._hoverState===p.IN&&c.show()},c.config.delay.show)):void c.show())},j.prototype._leave=function(b,c){var d=this.constructor.DATA_KEY;if(c=c||a(b.currentTarget).data(d),c||(c=new this.constructor(b.currentTarget,this._getDelegateConfig()),a(b.currentTarget).data(d,c)),b&&(c._activeTrigger["focusout"===b.type?u.FOCUS:u.HOVER]=!1),!c._isWithActiveTrigger())return clearTimeout(c._timeout),c._hoverState=p.OUT,c.config.delay&&c.config.delay.hide?void(c._timeout=setTimeout(function(){c._hoverState===p.OUT&&c.hide()},c.config.delay.hide)):void c.hide()},j.prototype._isWithActiveTrigger=function(){for(var a in this._activeTrigger)if(this._activeTrigger[a])return!0;return!1},j.prototype._getConfig=function(c){return c=a.extend({},this.constructor.Default,a(this.element).data(),c),c.delay&&"number"==typeof c.delay&&(c.delay={show:c.delay,hide:c.delay}),f.typeCheckConfig(b,c,this.constructor.DefaultType),c},j.prototype._getDelegateConfig=function(){var a={};if(this.config)for(var b in this.config)this.constructor.Default[b]!==this.config[b]&&(a[b]=this.config[b]);return a},j._jQueryInterface=function(b){return this.each(function(){var c=a(this).data(h),e="object"===("undefined"==typeof b?"undefined":d(b))?b:null;if((c||!/dispose|hide/.test(b))&&(c||(c=new j(this,e),a(this).data(h,c)),"string"==typeof b)){if(void 0===c[b])throw new Error('No method named "'+b+'"');c[b]()}})},e(j,null,[{key:"VERSION",get:function(){return g}},{key:"Default",get:function(){return m}},{key:"NAME",get:function(){return b}},{key:"DATA_KEY",get:function(){return h}},{key:"Event",get:function(){return q}},{key:"EVENT_KEY",get:function(){return i}},{key:"DefaultType",get:function(){return n}}]),j}();return a.fn[b]=v._jQueryInterface,a.fn[b].Constructor=v,a.fn[b].noConflict=function(){return a.fn[b]=j,v._jQueryInterface},v}(jQuery));(function(f){var h="popover",i="4.0.0-alpha.5",j="bs.popover",k="."+j,l=f.fn[h],m=f.extend({},g.Default,{placement:"right",trigger:"click",content:"",template:''}),n=f.extend({},g.DefaultType,{content:"(string|element|function)"}),o={FADE:"fade",IN:"in"},p={TITLE:".popover-title",CONTENT:".popover-content"},q={HIDE:"hide"+k,HIDDEN:"hidden"+k,SHOW:"show"+k,SHOWN:"shown"+k,INSERTED:"inserted"+k,CLICK:"click"+k,FOCUSIN:"focusin"+k,FOCUSOUT:"focusout"+k,MOUSEENTER:"mouseenter"+k,MOUSELEAVE:"mouseleave"+k},r=function(g){function l(){return c(this,l),a(this,g.apply(this,arguments))}return b(l,g),l.prototype.isWithContent=function(){return this.getTitle()||this._getContent()},l.prototype.getTipElement=function(){return this.tip=this.tip||f(this.config.template)[0]},l.prototype.setContent=function(){var a=f(this.getTipElement());this.setElementContent(a.find(p.TITLE),this.getTitle()),this.setElementContent(a.find(p.CONTENT),this._getContent()),a.removeClass(o.FADE).removeClass(o.IN),this.cleanupTether()},l.prototype._getContent=function(){return this.element.getAttribute("data-content")||("function"==typeof this.config.content?this.config.content.call(this.element):this.config.content)},l._jQueryInterface=function(a){return this.each(function(){var b=f(this).data(j),c="object"===("undefined"==typeof a?"undefined":d(a))?a:null;if((b||!/destroy|hide/.test(a))&&(b||(b=new l(this,c),f(this).data(j,b)),"string"==typeof a)){if(void 0===b[a])throw new Error('No method named "'+a+'"');b[a]()}})},e(l,null,[{key:"VERSION",get:function(){return i}},{key:"Default",get:function(){return m}},{key:"NAME",get:function(){return h}},{key:"DATA_KEY",get:function(){return j}},{key:"Event",get:function(){return q}},{key:"EVENT_KEY",get:function(){return k}},{key:"DefaultType",get:function(){return n}}]),l}(g);return f.fn[h]=r._jQueryInterface,f.fn[h].Constructor=r,f.fn[h].noConflict=function(){return f.fn[h]=l,r._jQueryInterface},r})(jQuery)}(); \ No newline at end of file diff --git a/Resources/public/js/symfonyProfiler.js b/Resources/public/js/symfonyProfiler.js new file mode 100644 index 00000000..a9ed4ef9 --- /dev/null +++ b/Resources/public/js/symfonyProfiler.js @@ -0,0 +1,174 @@ +/** + * Clear the state field and remove the checkbox + * @param key + */ +function clearState(key) { + var row = document.getElementById(key); + var cell = row.getElementsByClassName("state"); + cell[0].innerHTML = ""; + + // disable the checkbox + var inputs = row.getElementsByTagName("input"); + for (var i = 0; i < inputs.length; i++) { + if (inputs[i].type == "checkbox" && inputs[i].name == "translationKey") { + inputs[i].checked = false; + inputs[i].disabled = true; + } + } +} + +function flagMessage(key) { + Sfjs.request( + translationFlagUrl, + function(xhr) { + // Success + var el = document.getElementById(key).getElementsByClassName("flag"); + el[0].innerHTML = "Flag - OK"; + }, + function(xhr) { + // Error + console.log("Flagging message "+key+" - Error"); + }, + serializeQueryString({message_id: key}), + { method: 'POST' } + ); +} + +function syncMessage(key) { + Sfjs.request( + translationSyncUrl, + function(xhr) { + // Success + var el = document.getElementById(key).getElementsByClassName("translation"); + el[0].innerHTML = xhr.responseText; + + if (xhr.responseText !== "") { + clearState(key); + } + }, + function(xhr) { + // Error + console.log("Syncing message "+key + " - Error"); + }, + serializeQueryString({message_id: key}), + { method: 'POST' } + ); +} +function syncAll() { + Sfjs.request( + translationSyncAllUrl, + function(xhr) { + // Success + var el = document.getElementById("top-result-area"); + el.innerHTML = xhr.responseText; + }, + function(xhr) { + // Error + console.log("Syncing message "+key + " - Error"); + }, + {}, + { method: 'POST' } + ); +} + +function getEditForm(key) { + + Sfjs.request( + translationEditUrl + "?" + serializeQueryString({message_id: key}), + function(xhr) { + // Success + var el = document.getElementById(key).getElementsByClassName("translation"); + el[0].innerHTML = xhr.responseText; + }, + function(xhr) { + // Error + console.log("Getting edit form "+key+" - Error"); + }, + { method: 'GET' } + ); +} + +function saveEditForm(key, translation) { + + Sfjs.request( + translationEditUrl, + function(xhr) { + // Success + var el = document.getElementById(key).getElementsByClassName("translation"); + el[0].innerHTML = xhr.responseText; + + if (xhr.responseText !== "") { + clearState(key); + } + }, + function(xhr) { + // Error + console.log("Saving edit form "+key +" - Error"); + }, + serializeQueryString({message_id: key, translation:translation}), + { method: 'POST' } + ); + + return false; +} + +function cancelEditForm(key, orgMessage) { + var el = document.getElementById(key).getElementsByClassName("translation"); + el[0].innerHTML = orgMessage; +} + +var serializeQueryString = function(obj, prefix) { + var str = []; + for(var p in obj) { + if (obj.hasOwnProperty(p)) { + var k = prefix ? prefix + "[" + p + "]" : p, v = obj[p]; + str.push(typeof v == "object" ? serializeQueryString(v, k) : encodeURIComponent(k) + "=" + encodeURIComponent(v)); + } + } + 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"; + if (typeof(form.translationKey) === 'undefined') { + return false; + } + var inputs = form.translationKey; + var selected = []; + if (!inputs.value) { + for (var val in inputs) { + if (inputs.hasOwnProperty(val) && inputs[val].value) { + if (inputs[val].checked) { + selected.push(inputs[val].value); + } + } + } + } else if (inputs.checked) { + selected.push(inputs.value); + } + Sfjs.request( + form.action, + function(xhr) { + // Success + document.getElementById('translationResult').innerHTML = xhr.responseText; + }, + function(xhr) { + // Error + document.getElementById('translationResult').innerHTML = xhr.responseText; + }, + serializeQueryString({selected: selected}), + { method: 'POST' } + ); + return false; +}; diff --git a/Resources/views/SymfonyProfiler/edit.html.twig b/Resources/views/SymfonyProfiler/edit.html.twig index 63a34c66..51eb5692 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/javascripts.html.twig b/Resources/views/SymfonyProfiler/javascripts.html.twig index fa9465c4..e8a069eb 100644 --- a/Resources/views/SymfonyProfiler/javascripts.html.twig +++ b/Resources/views/SymfonyProfiler/javascripts.html.twig @@ -1,175 +1,10 @@ - var serializeQueryString = function(obj, prefix) { - var str = []; - for(var p in obj) { - if (obj.hasOwnProperty(p)) { - var k = prefix ? prefix + "[" + p + "]" : p, v = obj[p]; - str.push(typeof v == "object" ? serializeQueryString(v, k) : encodeURIComponent(k) + "=" + encodeURIComponent(v)); - } - } - 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"; - if (typeof(form.translationKey) === 'undefined') { - return false; - } - var inputs = form.translationKey; - var selected = []; - if (!inputs.value) { - for (var val in inputs) { - if (inputs.hasOwnProperty(val) && inputs[val].value) { - if (inputs[val].checked) { - selected.push(inputs[val].value); - } - } - } - } else if (inputs.checked) { - selected.push(inputs.value); - } - Sfjs.request( - form.action, - function(xhr) { - // Success - document.getElementById('translationResult').innerHTML = xhr.responseText; - }, - function(xhr) { - // Error - document.getElementById('translationResult').innerHTML = xhr.responseText; - }, - serializeQueryString({selected: selected}), - { method: 'POST' } - ); - return false; - }; - + diff --git a/Resources/views/SymfonyProfiler/translation.html.twig b/Resources/views/SymfonyProfiler/translation.html.twig index c4da2503..bf22703a 100644 --- a/Resources/views/SymfonyProfiler/translation.html.twig +++ b/Resources/views/SymfonyProfiler/translation.html.twig @@ -45,7 +45,7 @@ {% endif %} {% endfor %} -
@@ -114,7 +114,7 @@
- {% include "HappyrTranslationBundle:Profiler:javascripts.html.twig" %} + {% include "@Translation/SymfonyProfiler/javascripts.html.twig" %} {% endblock %} {% macro render_table(messages) %} @@ -166,8 +166,6 @@ {% spaceless %} Edit | - Flag - | Sync {% endspaceless %} diff --git a/Service/CatalogueFetcher.php b/Service/CatalogueFetcher.php index 53e14441..0a913b0c 100644 --- a/Service/CatalogueFetcher.php +++ b/Service/CatalogueFetcher.php @@ -18,6 +18,8 @@ * Fetches catalogues from source files. * * @author Tobias Nyholm + * + * @deprecated I think this could be removed.. Not sure. */ class CatalogueFetcher { diff --git a/Service/StorageService.php b/Service/StorageService.php new file mode 100644 index 00000000..93201218 --- /dev/null +++ b/Service/StorageService.php @@ -0,0 +1,229 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Translation\Bundle\Service; + +use Translation\Common\Exception\LogicException; +use Translation\Common\Model\Message; +use Translation\Common\Storage; + +/** + * A service that you use to handle the storages. + * + * @author Tobias Nyholm + */ +class StorageService implements Storage +{ + const DIRECTION_UP = 'up'; + const DIRECTION_DOWN = 'down'; + + /** + * @var Storage[] + */ + private $localStorages = []; + + /** + * @var Storage[] + */ + private $remoteStorages = []; + + /** + * Download all remote storages into all local storages. + * This will overwrite your local copy. + */ + public function download() + { + // TODO + } + + /** + * Upload all local storages into all remote storages + * This will overwrite your remote copy. + */ + public function upload() + { + // TODO + } + + /** + * Synchronize translations with remote. + */ + public function sync($direction = self::DIRECTION_DOWN) + { + switch ($direction) { + case self::DIRECTION_DOWN: + $this->mergeDown(); + $this->mergeUp(); + break; + case self::DIRECTION_UP: + $this->mergeUp(); + $this->mergeDown(); + break; + default: + throw new LogicException(sprintf('Direction must be either "up" or "down". Value "%s" was provided', $direction)); + } + } + + /** + * Download and merge all translations from remote storages down to your local storages. + * Only the local storages will be changed. + */ + public function mergeDown() + { + // TODO + } + + /** + * Upload and merge all translations from local storages up to your remote storages. + * Only the remote storages will be changed. + */ + public function mergeUp() + { + // TODO + } + + /** + * Get the very latest version we know of a message. First look at the remote storage + * fall back on the local ones. + * + * @param string $locale + * @param string $domain + * @param string $key + * + * @return null|Message + */ + public function syncAndFetchMessage($locale, $domain, $key) + { + $message = $this->getFromStorages($this->remoteStorages, $locale, $domain, $key); + if (!$message) { + // If message is not in remote storages + $message = $this->getFromStorages($this->localStorages, $locale, $domain, $key); + } + + $this->updateStorages($this->localStorages, $message); + + return $message; + } + + /** + * Try to get a translation from all the storages, start looking in the first + * local storage and then move on to the remote storages. + * {@inheritdoc} + */ + public function get($locale, $domain, $key) + { + foreach ([$this->localStorages, $this->remoteStorages] as $storages) { + $value = $this->getFromStorages($storages, $locale, $domain, $key); + if (!empty($value)) { + return $value; + } + } + + return; + } + + /** + * @param Storage[] $storages + * @param string $locale + * @param string $domain + * @param string $key + * + * @return null|Message + */ + private function getFromStorages($storages, $locale, $domain, $key) + { + foreach ($storages as $storage) { + $value = $storage->get($locale, $domain, $key); + if (!empty($value)) { + return $value; + } + } + + return; + } + + /** + * Update all configured storages with this message. + * + * {@inheritdoc} + */ + public function update(Message $message) + { + foreach ([$this->localStorages, $this->remoteStorages] as $storages) { + $this->updateStorages($storages, $message); + } + } + + /** + * @param Storage[] $storages + * @param Message $message + */ + private function updateStorages($storages, Message $message) + { + // Validate if message actually has data + if (empty((array) $message)) { + return; + } + + foreach ($storages as $storage) { + $storage->update($message); + } + } + + /** + * Delete the message form all storages. + * + * {@inheritdoc} + */ + public function delete($locale, $domain, $key) + { + foreach ([$this->localStorages, $this->remoteStorages] as $storages) { + $this->deleteFromStorages($storages, $locale, $domain, $key); + } + } + + /** + * @param Storage[] $storages + * @param string $locale + * @param string $domain + * @param string $key + */ + private function deleteFromStorages($storages, $locale, $domain, $key) + { + foreach ($storages as $storage) { + $storage->delete($locale, $domain, $key); + } + } + + /** + * @param Storage $localStorage + * + * @return StorageService + */ + public function addLocalStorage(Storage $localStorage) + { + $this->localStorages[] = $localStorage; + + return $this; + } + + /** + * @param Storage $remoteStorages + * + * @return StorageService + */ + public function addRemoteStorage(Storage $remoteStorage) + { + $this->remoteStorages[] = $remoteStorage; + + return $this; + } +} diff --git a/Storage/FileStorage.php b/Storage/FileStorage.php index 8e6f234d..303bb0a0 100644 --- a/Storage/FileStorage.php +++ b/Storage/FileStorage.php @@ -14,7 +14,7 @@ use Symfony\Bundle\FrameworkBundle\Translation\TranslationLoader; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Writer\TranslationWriter; -use Translation\Common\Exception\StorageException; +use Translation\Common\Model\Message; use Translation\Common\Storage; /** @@ -56,31 +56,31 @@ public function __construct(TranslationWriter $writer, TranslationLoader $loader $this->dir = $dir; } - public function set($locale, $domain, $key, $message) - { - $originalMessage = $this->get($locale, $domain, $key); - if (!empty($originalMessage)) { - throw StorageException::translationExists($key, $domain); - } - - $catalogue = $this->getCatalogue($locale); - $catalogue->set($key, $message, $domain); - } - + /** + * {@inheritdoc} + */ public function get($locale, $domain, $key) { $catalogue = $this->getCatalogue($locale); - return $catalogue->get($key, $domain); + $translation = $catalogue->get($key, $domain); + + return new Message($key, $domain, $locale, $translation); } - public function update($locale, $domain, $key, $message) + /** + * {@inheritdoc} + */ + public function update(Message $message) { - $catalogue = $this->getCatalogue($locale); - $catalogue->set($key, $message, $domain); - $this->writeCatalogue($catalogue, $locale, $domain); + $catalogue = $this->getCatalogue($message->getLocale()); + $catalogue->set($message->getKey(), $message->getTranslation(), $message->getDomain()); + $this->writeCatalogue($catalogue, $message->getLocale(), $message->getDomain()); } + /** + * {@inheritdoc} + */ public function delete($locale, $domain, $key) { $catalogue = $this->getCatalogue($locale); diff --git a/Tests/Functional/app/AppKernel.php b/Tests/Functional/app/AppKernel.php index 4813ddbb..22c35ce3 100644 --- a/Tests/Functional/app/AppKernel.php +++ b/Tests/Functional/app/AppKernel.php @@ -53,7 +53,7 @@ public function registerContainerConfiguration(LoaderInterface $loader) public function getCacheDir() { - return sys_get_temp_dir().'/HappyrMq2PHPBundle'; + return sys_get_temp_dir().'/TranslationBundle'; } public function serialize() diff --git a/TranslationBundle.php b/TranslationBundle.php index 6c0e2675..a5ba0939 100644 --- a/TranslationBundle.php +++ b/TranslationBundle.php @@ -15,12 +15,17 @@ use Symfony\Component\HttpKernel\Bundle\Bundle; use Translation\Bundle\DependencyInjection\CompilerPass\ExternalTranslatorPass; use Translation\Bundle\DependencyInjection\CompilerPass\ExtractorPass; +use Translation\Bundle\DependencyInjection\CompilerPass\StoragePass; +/** + * @author Tobias Nyholm + */ class TranslationBundle extends Bundle { public function build(ContainerBuilder $container) { $container->addCompilerPass(new ExternalTranslatorPass()); $container->addCompilerPass(new ExtractorPass()); + $container->addCompilerPass(new StoragePass()); } }