From 7aa7cb2fa4d5c90fad43564b688e1b4b499bb7f3 Mon Sep 17 00:00:00 2001 From: Bozhidar Hristov Date: Wed, 8 Feb 2017 16:07:31 +0200 Subject: [PATCH] Fixes #1 --- src/Command/ExportTranslationsCommand.php | 145 ++++++++++++++++ src/Command/ImportTranslationsCommand.php | 155 ++++++++++++++++++ src/Controller/CRUDController.php | 17 +- .../OverrideTranslatorCompilerPass.php | 1 - .../Compiler/TemplatingCompilerPass.php | 1 - src/DependencyInjection/Configuration.php | 1 - .../ObjectBGTranslationExtension.php | 3 +- src/Doctrine/Filter/LanguageFilter.php | 2 +- src/Entity/Language.php | 14 +- src/Entity/LanguageRepository.php | 10 ++ src/Entity/Translation.php | 2 +- .../TranslationRepository.php} | 19 ++- src/Entity/TranslationToken.php | 2 +- src/Entity/TranslationTokenRepository.php | 47 ++++++ .../CurrentTranslationLoader.php | 62 +++---- src/Exception/InvalidArgumentException.php | 9 +- src/Repository/Language.php | 10 -- src/Repository/TranslationToken.php | 10 -- src/Resources/config/services.xml | 1 - src/TranslationLoader.php | 42 ----- src/TranslationService.php | 3 +- 21 files changed, 434 insertions(+), 122 deletions(-) create mode 100644 src/Command/ExportTranslationsCommand.php create mode 100644 src/Command/ImportTranslationsCommand.php create mode 100644 src/Entity/LanguageRepository.php rename src/{Repository/Translation.php => Entity/TranslationRepository.php} (75%) create mode 100644 src/Entity/TranslationTokenRepository.php delete mode 100644 src/Repository/Language.php delete mode 100644 src/Repository/TranslationToken.php delete mode 100644 src/TranslationLoader.php diff --git a/src/Command/ExportTranslationsCommand.php b/src/Command/ExportTranslationsCommand.php new file mode 100644 index 0000000..da18488 --- /dev/null +++ b/src/Command/ExportTranslationsCommand.php @@ -0,0 +1,145 @@ +setName('objectbg:translation:export') + ->setDefinition( + array( + new InputArgument('locale', InputArgument::REQUIRED, 'The locale'), + new InputArgument( + 'bundle', + InputArgument::OPTIONAL, + 'The bundle name or directory where to load the messages, defaults to app/Resources folder' + ), + new InputOption('format', 'f', InputOption::VALUE_OPTIONAL, 'Force the output format.', 'xlf'), + new InputOption('clean', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_NONE), + ) + ); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->output = $output; + $this->io = new SymfonyStyle($input, $output); + $kernel = $this->getContainer()->get('kernel'); + + $format = $this->input->getOption('format'); + $clean = $this->input->getOption('clean'); + $locale = $this->input->getArgument('locale'); + + $bundleName = $this->input->getArgument('bundle'); + if ($bundleName) { + $bundle = $kernel->getBundle($bundleName); + $transPaths = $bundle->getPath() . '/Resources/translations'; + } + $this->exportFile($transPaths, $locale, $format, $clean); + } + + private function exportFile($transPaths, $locale, $format, $clean) + { + $db = $this->exportFromDB($locale); + $language = $this->getContainer()->get('doctrine')->getManager()->getRepository(Language::class)->findOneBy( + ['locale' => $locale] + ); + $currentCatalogue = $this->extractMessages($locale, $transPaths); + $extractedCatalogue = new MessageCatalogue($locale); + if ($db != null) { + foreach ($db as $token) { + $translation = $token->getTranslation($language)->getTranslation(); + if (!$translation) { + $translation = $token->getToken(); + } + $extractedCatalogue->set($token->getToken(), $translation, $token->getCatalogue()); + } + } else { + $this->output->writeln('No translations to export.'); + + return; + } + $writer = $this->getContainer()->get('translation.writer'); + $supportedFormats = $writer->getFormats(); + if (!in_array($format, $supportedFormats)) { + $this->io->error( + array('Wrong output format', 'Supported formats are: ' . implode(', ', $supportedFormats) . '.') + ); + + return 1; + } + + $operation = $clean ? new TargetOperation($currentCatalogue, $extractedCatalogue) : new MergeOperation( + $currentCatalogue, $extractedCatalogue + ); + + $writer->writeTranslations( + $operation->getResult(), + $format, + array( + 'path' => $transPaths, + 'default_locale' => $this->getContainer()->getParameter('kernel.default_locale'), + ) + ); + } + + /** + * + * @param string $locale + * @return TranslationToken[] + */ + private function exportFromDB($locale) + { + $em = $this->getContainer()->get('doctrine')->getManager(); + + return $em->getRepository(TranslationToken::class)->getAllTokensByLocale($locale); + } + + /** + * + * @param type $locale + * @param type $transPaths + * @return MessageCatalogue + */ + private function extractMessages($locale, $transPaths) + { + /** @var TranslationLoader $loader */ + $loader = $this->getContainer()->get('translation.loader'); + $currentCatalogue = new MessageCatalogue($locale); + + if (is_dir($transPaths)) { + $loader->loadMessages($transPaths, $currentCatalogue); + } + + return $currentCatalogue; + } + +} diff --git a/src/Command/ImportTranslationsCommand.php b/src/Command/ImportTranslationsCommand.php new file mode 100644 index 0000000..2053a83 --- /dev/null +++ b/src/Command/ImportTranslationsCommand.php @@ -0,0 +1,155 @@ +setName('objectbg:translation:import') + ->setDefinition( + array( + new InputArgument('locale', InputArgument::REQUIRED, 'The locale'), + new InputArgument( + 'bundle', + InputArgument::OPTIONAL, + 'The bundle name or directory where to load the messages, defaults to app/Resources folder' + ), + new InputOption('all', null, InputOption::VALUE_NONE, 'Load messages from all registered bundles'), + new InputOption('override', null, InputOption::VALUE_NONE, 'Should the update be done'), + ) + ) + ->setDescription('Displays translation messages information') + ->setHelp( + <<<'EOF' + The %command.name% command helps finding unused or missing translation +messages and comparing them with the fallback ones by inspecting the +templates and translation files of a given bundle or the app folder. +You can display information about bundle translations in a specific locale: + php %command.full_name% en AcmeDemoBundle +You can also specify a translation domain for the search: + php %command.full_name% --domain=messages en AcmeDemoBundle +You can display information about app translations in a specific locale: + php %command.full_name% en +You can display information about translations in all registered bundles in a specific locale: + php %command.full_name% --all en +EOF + ); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->output = $output; + + $kernel = $this->getContainer()->get('kernel'); + $transPaths = array($kernel->getRootDir() . '/Resources/'); + + $locale = $this->input->getArgument('locale'); + $override = $this->input->getOption('override'); + + + $bundleName = $this->input->getArgument('bundle'); + if ($bundleName) { + $bundle = $kernel->getBundle($bundleName); + $transPaths[] = $bundle->getPath() . '/Resources/'; + $transPaths[] = sprintf('%s/Resources/%s/', $kernel->getRootDir(), $bundle->getName()); + } elseif ($input->getOption('all')) { + foreach ($kernel->getBundles() as $bundle) { + $transPaths[] = $bundle->getPath() . '/Resources/'; + $transPaths[] = sprintf('%s/Resources/%s/', $kernel->getRootDir(), $bundle->getName()); + } + } + + $catalogue = $this->extractMessages($locale, $transPaths); + $this->importTranslationFiles($catalogue, $locale, $override); + } + + /** + * @param string $locale + * @param array $transPaths + * + * @return MessageCatalogue + */ + private function extractMessages($locale, $transPaths) + { + /** @var TranslationLoader $loader */ + $loader = $this->getContainer()->get('translation.loader'); + + $currentCatalogue = new MessageCatalogue($locale); + foreach ($transPaths as $path) { + $path = $path . 'translations'; + if (is_dir($path)) { + $loader->loadMessages($path, $currentCatalogue); + } + } + + return $currentCatalogue; + } + + public function importTranslationFiles(MessageCatalogue $messages, $locale, $override) + { + $domains = $messages->all(); + $translationToken = null; + $translation = null; + + $em = $this->getContainer()->get('doctrine')->getManager(); + $language = $em->getRepository(Language::class)->findOneBy(['locale' => $locale]); + + /** @var TranslationTokenRepository $transTokenRepo */ + $transTokenRepo = $em->getRepository(TranslationToken::class); + /** @var TranslationRepository $transRepo */ + $transRepo = $em->getRepository(Translation::class); + + foreach ($domains as $catalogue => $messages) { + foreach ($messages as $token => $val) { + $translationToken = $transTokenRepo->findByTokenAndCatalogue($token, $catalogue); + if (!$translationToken) { + $translationToken = new TranslationToken(); + $translationToken->setToken($token); + $translationToken->setCatalogue($catalogue); + } else { + $translation = $transRepo->getTranslationByTokenAndLanguage($translationToken, $language); + } + + if (!$translation || $override) { + if (!$translation) { + $translation = new Translation(); + } + $translation->setLanguage($language); + $translation->setTranslationToken($translationToken); + $translation->setTranslation($val); + $em->persist($translationToken); + $em->persist($translation); + } + } + } + $em->flush(); + } + +} diff --git a/src/Controller/CRUDController.php b/src/Controller/CRUDController.php index 7dc39b1..55d4ed5 100644 --- a/src/Controller/CRUDController.php +++ b/src/Controller/CRUDController.php @@ -2,13 +2,16 @@ namespace ObjectBG\TranslationBundle\Controller; +use ObjectBG\TranslationBundle\Entity\Translation; +use Sonata\AdminBundle\Controller\CRUDController as BaseCRUDController; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; +use Symfony\Component\HttpFoundation\Request; -class CRUDController extends \Sonata\AdminBundle\Controller\CRUDController +class CRUDController extends BaseCRUDController { - public function listAction(\Symfony\Component\HttpFoundation\Request $request = null) + public function listAction(Request $request = null) { $canEdit = $this->admin->isGranted('EDIT'); $canView = $this->admin->isGranted('LIST'); @@ -66,14 +69,6 @@ public function listAction(\Symfony\Component\HttpFoundation\Request $request = $tokens = $qb->getQuery()->getResult(); $formBuilder = $this->createFormBuilder(); -// $FormBuilder->add('tokens', 'collection', array( -// 'type' => 'text', -// 'label' => false, -// 'allow_add' => true, -// 'options' => array( -// 'label' => false -// ) -// )); $formBuilder->add( 'translations', @@ -151,7 +146,7 @@ function ($item) use ($token, $language) { continue; } if (!$translation) { - $translation = new \ObjectBG\TranslationBundle\Entity\Translation(); + $translation = new Translation(); $translation->setLanguage($language); $translation->setTranslationToken($token); } diff --git a/src/DependencyInjection/Compiler/OverrideTranslatorCompilerPass.php b/src/DependencyInjection/Compiler/OverrideTranslatorCompilerPass.php index 6cdd920..024a0ee 100644 --- a/src/DependencyInjection/Compiler/OverrideTranslatorCompilerPass.php +++ b/src/DependencyInjection/Compiler/OverrideTranslatorCompilerPass.php @@ -7,7 +7,6 @@ class OverrideTranslatorCompilerPass implements CompilerPassInterface { - /** * {@inheritdoc} */ diff --git a/src/DependencyInjection/Compiler/TemplatingCompilerPass.php b/src/DependencyInjection/Compiler/TemplatingCompilerPass.php index 23a678e..3a75a85 100644 --- a/src/DependencyInjection/Compiler/TemplatingCompilerPass.php +++ b/src/DependencyInjection/Compiler/TemplatingCompilerPass.php @@ -7,7 +7,6 @@ class TemplatingCompilerPass implements CompilerPassInterface { - /** * {@inheritdoc} */ diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index fa4c6c5..226d048 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -12,7 +12,6 @@ */ class Configuration implements ConfigurationInterface { - /** * {@inheritDoc} */ diff --git a/src/DependencyInjection/ObjectBGTranslationExtension.php b/src/DependencyInjection/ObjectBGTranslationExtension.php index 4de78e8..a4ce7a2 100644 --- a/src/DependencyInjection/ObjectBGTranslationExtension.php +++ b/src/DependencyInjection/ObjectBGTranslationExtension.php @@ -14,7 +14,6 @@ */ class ObjectBGTranslationExtension extends Extension { - /** * {@inheritDoc} */ @@ -23,7 +22,7 @@ public function load(array $configs, ContainerBuilder $container) $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('services.xml'); $loader->load('admins.xml'); } diff --git a/src/Doctrine/Filter/LanguageFilter.php b/src/Doctrine/Filter/LanguageFilter.php index 910f2e8..96d56b4 100644 --- a/src/Doctrine/Filter/LanguageFilter.php +++ b/src/Doctrine/Filter/LanguageFilter.php @@ -13,6 +13,6 @@ public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAli return ""; } - return $targetTableAlias.'.locale = '.$this->getParameter('locale'); + return $targetTableAlias . '.locale = ' . $this->getParameter('locale'); } } diff --git a/src/Entity/Language.php b/src/Entity/Language.php index 49b4eb0..17f76b2 100644 --- a/src/Entity/Language.php +++ b/src/Entity/Language.php @@ -4,9 +4,10 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Validator\Constraints as Assert; /** - * @ORM\Entity(repositoryClass="ObjectBG\TranslationBundle\Repository\Language") + * @ORM\Entity(repositoryClass="ObjectBG\TranslationBundle\Entity\LanguageRepository") * @ORM\Table(name="languages") * @UniqueEntity(fields={"locale"}, message="This locale already exists") * @UniqueEntity(fields={"name"}, message="This name already exists") @@ -21,10 +22,17 @@ class Language */ private $id; - /** @ORM\column(type="string", length=200, unique=true) */ + /** + * @Assert\Locale() + * @Assert\NotBlank + * @ORM\Column(type="string", length=200, unique=true) + */ private $locale; - /** @ORM\column(type="string", length=200, unique=true) */ + /** + * @Assert\NotBlank + * @ORM\column(type="string", length=200, unique=true) + */ private $name; public function getId() diff --git a/src/Entity/LanguageRepository.php b/src/Entity/LanguageRepository.php new file mode 100644 index 0000000..59f0f6d --- /dev/null +++ b/src/Entity/LanguageRepository.php @@ -0,0 +1,10 @@ +getEntityManager(); + $dql = "SELECT t FROM ObjectBGTranslationBundle:Translation t WHERE t.translationToken = :token AND t.language = :language"; + + $exists = $em->createQuery($dql) + ->setParameter('token', $translationToken) + ->setParameter('language', $language) + ->getOneOrNullResult(); + + return $exists; + } } diff --git a/src/Entity/TranslationToken.php b/src/Entity/TranslationToken.php index fe114c9..9b67998 100644 --- a/src/Entity/TranslationToken.php +++ b/src/Entity/TranslationToken.php @@ -6,7 +6,7 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; /** - * @ORM\Entity(repositoryClass="ObjectBG\TranslationBundle\Repository\TranslationToken") + * @ORM\Entity(repositoryClass="ObjectBG\TranslationBundle\Entity\TranslationTokenRepository") * @ORM\Table(name="translation_tokens", * uniqueConstraints={@ORM\UniqueConstraint(columns={"token", "catalogue"})} * ) diff --git a/src/Entity/TranslationTokenRepository.php b/src/Entity/TranslationTokenRepository.php new file mode 100644 index 0000000..aac5d3c --- /dev/null +++ b/src/Entity/TranslationTokenRepository.php @@ -0,0 +1,47 @@ +getEntityManager(); + $dql = 'SELECT COUNT(token) FROM ObjectBGTranslationBundle:TranslationToken token WHERE token.token = :token AND token.catalogue = :catalogue'; + + + $exists = ((int)$em->createQuery($dql) + ->setParameter('token', $token) + ->setParameter('catalogue', $catalogue) + ->getSingleScalarResult()) > 0; + + return $exists; + } + + public function findByTokenAndCatalogue($token, $catalogue) + { + $em = $this->getEntityManager(); + $dql = 'SELECT token FROM ObjectBGTranslationBundle:TranslationToken token WHERE token.token = :token AND token.catalogue = :catalogue'; + + + $exists = $em->createQuery($dql) + ->setParameter('token', $token) + ->setParameter('catalogue', $catalogue) + ->getOneOrNullResult(); + + return $exists; + } + + public function getAllTokensByLocale($locale) + { + $em = $this->getEntityManager(); + $dql = 'SELECT token, translation FROM ObjectBGTranslationBundle:TranslationToken token JOIN token.translations translation JOIN translation.language language WITH language.locale = :locale'; + $exists = $em->createQuery($dql) + ->setParameter('locale', $locale) + ->getResult(); + + return $exists; + } +} diff --git a/src/EventListener/CurrentTranslationLoader.php b/src/EventListener/CurrentTranslationLoader.php index 8b63d51..0ef2737 100644 --- a/src/EventListener/CurrentTranslationLoader.php +++ b/src/EventListener/CurrentTranslationLoader.php @@ -3,6 +3,7 @@ namespace ObjectBG\TranslationBundle\EventListener; use Doctrine\Common\EventSubscriber; +use ObjectBG\TranslationBundle\Entity\Language; use ObjectBG\TranslationBundle\TranslatableInterface; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -12,27 +13,29 @@ class CurrentTranslationLoader implements EventSubscriber { /** - * * @var Container */ - private $Container; + private $container; /** - * * @var PropertyAccessor */ - private $PropertyAccess; - private $Fallback = true; + private $propertyAccessor; - public function __construct(Container $Container) + /** + * @var bool + */ + private $fallback = true; + + public function __construct(Container $container) { - $this->Container = $Container; - $this->PropertyAccess = PropertyAccess::createPropertyAccessor(); + $this->container = $container; + $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); } public function doFallback($trueFalse) { - $this->Fallback = $trueFalse; + $this->fallback = $trueFalse; } public function getSubscribedEvents() @@ -52,19 +55,19 @@ public function postLoad($Event) public function initializeCurrentTranslation($Entity) { - $TranslationService = $this->Container->get('object_bg.translation.service.translation'); - $CurrentLanguage = $TranslationService->getCurrentLanguage(); + $translationService = $this->container->get('object_bg.translation.service.translation'); + $CurrentLanguage = $translationService->getCurrentLanguage(); $success = $this->initializeTranslation($Entity, $CurrentLanguage); - if ($success == false && $this->Fallback === true) { + if ($success == false && $this->fallback === true) { $this->initializeFallbackTranslation($Entity); } } private function initializeFallbackTranslation($Entity) { - $TranslationService = $this->Container->get('object_bg.translation.service.translation'); - $fallbacks = $TranslationService->getFallbackLocales(); + $translationService = $this->container->get('object_bg.translation.service.translation'); + $fallbacks = $translationService->getFallbackLocales(); foreach ($fallbacks as $fallback) { if ($this->initializeTranslation($Entity, $fallback)) { @@ -73,35 +76,38 @@ private function initializeFallbackTranslation($Entity) } } - public function initializeTranslation($Entity, $Language) + public function initializeTranslation($entity, $languageOrLocale) { - if (!$Entity instanceof TranslatableInterface) { + if (!$entity instanceof TranslatableInterface) { throw new \RuntimeException('Entity is not translatable'); } - $TranslationService = $this->Container->get('object_bg.translation.service.translation'); + $translationService = $this->container->get('object_bg.translation.service.translation'); - $Translations = $this->PropertyAccess->getValue($Entity, $TranslationService->getTranslationsField($Entity)); + $translations = $this->propertyAccessor->getValue($entity, $translationService->getTranslationsField($entity)); - if (!$Translations) { + if (!$translations) { return false; } - $PropertyAccess = $this->PropertyAccess; + $propertyAccessor = $this->propertyAccessor; - $CurrentTranslation = $Translations->filter( - function ($item) use ($TranslationService, $Language, $PropertyAccess) { - $TranslationLanguage = $PropertyAccess->getValue($item, $TranslationService->getLanguageField($item)); + $currentTranslation = $translations->filter( + function ($item) use ($translationService, $languageOrLocale, $propertyAccessor) { + $translationLanguage = $propertyAccessor->getValue($item, $translationService->getLanguageField($item)); - return $Language instanceof \ObjectBG\TranslationBundle\Entity\Language ? ($TranslationLanguage == $Language) : ($TranslationLanguage->getLocale( - ) == $Language); + if ($languageOrLocale instanceof Language) { + return $translationLanguage == $languageOrLocale; + } else { + $translationLanguage->getLocale() == $languageOrLocale; + } } )->first(); - if (!$CurrentTranslation) { + if (!$currentTranslation) { return false; } - $CurrentTranslationField = $TranslationService->getCurrentTranslationField($Entity); - $this->PropertyAccess->setValue($Entity, $CurrentTranslationField, $CurrentTranslation); + $currentTranslationField = $translationService->getCurrentTranslationField($entity); + $this->propertyAccessor->setValue($entity, $currentTranslationField, $currentTranslation); return true; } diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index 84fb357..3478940 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -4,14 +4,13 @@ class InvalidArgumentException extends \InvalidArgumentException { - - public static function missingTranslations($TranslatableClass) + public static function missingTranslations($translatableClass) { - return new self('Missing translations association for entity '.$TranslatableClass); + return new self('Missing translations association for entity ' . $translatableClass); } - public static function missingRequiredAnnotation($Class, $Annotation) + public static function missingRequiredAnnotation($class, $annotation) { - return new self($Class.' is missing required annotation '.$Annotation); + return new self($class . ' is missing required annotation ' . $annotation); } } diff --git a/src/Repository/Language.php b/src/Repository/Language.php deleted file mode 100644 index ead824b..0000000 --- a/src/Repository/Language.php +++ /dev/null @@ -1,10 +0,0 @@ - - ObjectBG\TranslationBundle\TranslationLoader ObjectBG\TranslationBundle\Dumper\DatabaseDumper diff --git a/src/TranslationLoader.php b/src/TranslationLoader.php deleted file mode 100644 index 019d2fe..0000000 --- a/src/TranslationLoader.php +++ /dev/null @@ -1,42 +0,0 @@ -translationRepository = $entityManager->getRepository("ObjectBGTranslationBundle:Translation"); - $this->languageRepository = $entityManager->getRepository("ObjectBGTranslationBundle:Language"); - } - - public function load($resource, $locale, $domain = 'messages') - { - $catalogue = new MessageCatalogue($locale); - $language = $this->languageRepository->findOneByLocale($locale); - - if ($language) { - $translations = $this->translationRepository->getTranslations($language, $domain); - foreach ($translations as $translation) { - $catalogue->set( - $translation->getTranslationToken()->getToken(), - $translation->getTranslation(), - $domain - ); - } - } - - return $catalogue; - } -} diff --git a/src/TranslationService.php b/src/TranslationService.php index a56eca1..94478c4 100644 --- a/src/TranslationService.php +++ b/src/TranslationService.php @@ -14,7 +14,6 @@ class TranslationService { - private $typeGuesser; /** @@ -299,7 +298,7 @@ private function getFieldsList($options, $class) // Check existing foreach ($formFields as $field) { if (!property_exists($class, $field)) { - throw new \Exception("Field '".$field."' doesn't exist in ".$class); + throw new \Exception("Field '" . $field . "' doesn't exist in " . $class); } }