├── Changelog.md ├── LICENSE ├── README.md ├── composer.json ├── src ├── Command │ ├── DownloadCommand.php │ ├── SynchronizeCommand.php │ └── UploadCommand.php ├── Controller │ └── ProfilerController.php ├── DependencyInjection │ ├── Configuration.php │ └── HappyrTranslationExtension.php ├── Exception │ ├── HappyrTranslationException.php │ └── HttpException.php ├── HappyrTranslationBundle.php ├── Http │ └── RequestManager.php ├── Model │ └── Message.php ├── Resources │ ├── config │ │ ├── autoAdd.yml │ │ ├── routing_dev.yml │ │ └── services.yml │ ├── doc │ │ └── images │ │ │ ├── edit-flag-sync-example.gif │ │ │ ├── missing-translation-example.gif │ │ │ └── toolbar-example.png │ └── views │ │ └── Profiler │ │ ├── edit.html.twig │ │ ├── javascripts.html.twig │ │ └── translation.html.twig ├── Service │ ├── Blackhole.php │ ├── Filesystem.php │ ├── Loco.php │ └── TranslationServiceInterface.php └── Translation │ ├── AutoAdder.php │ └── FilesystemUpdater.php └── tests └── Functional ├── AppKernel.php ├── BaseTestCase.php ├── BundleInitializationTest.php └── config ├── default.yml ├── framework.yml └── routing.yml /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 0.4 to master 4 | 5 | ### 0.3 to 0.4 6 | 7 | * The command `happyr:translation:sync` does not crash when translation files do not exist. ([#26](https://github.com/Happyr/TranslationBundle/pull/26)) 8 | * Added command for uploading all translations. (`happyr:translation:upload`) ([#29](https://github.com/Happyr/TranslationBundle/pull/31)) 9 | * The value `yaml` for the configuration key `file_extension` has been renamed to `yml`. ([#31](https://github.com/Happyr/TranslationBundle/pull/31)) 10 | * Added option to not handle empty translations. ([#31](https://github.com/Happyr/TranslationBundle/pull/31)) 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Happyr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Happyr Translation Bundle 2 | 3 | [![Latest Version](https://img.shields.io/github/release/Happyr/TranslationBundle.svg?style=flat-square)](https://github.com/Happyr/TranslationBundle/releases) 4 | [![Build Status](https://img.shields.io/travis/Happyr/TranslationBundle.svg?style=flat-square)](https://travis-ci.org/Happyr/TranslationBundle) 5 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/Happyr/TranslationBundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/Happyr/TranslationBundle) 6 | [![Quality Score](https://img.shields.io/scrutinizer/g/Happyr/TranslationBundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/Happyr/TranslationBundle) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/happyr/translation-bundle.svg?style=flat-square)](https://packagist.org/packages/happyr/translation-bundle) 8 | 9 | 10 | # DEPRECATED: Use php-translation/symfony-bundle 11 | 12 | This bundle has been deprecated in favor of [php-tranlation/symfony-bundle](https://packagist.org/packages/php-translation/symfony-bundle). We took all features form this bundle and put them (and many more) at php-translation. 13 | 14 | The bundle will still live here forever but no new features or bugfixes will be merged. Forking or moving this repo to new maintainers will not make any sense since that is pretty much what we already done in php-translation. 15 | 16 | ------- 17 | 18 | This bundle helps you to integrate with a third party translation service. The bundle has been focused to integrate to 19 | the [Loco](https://localise.biz) service. If you want to know how Happyr work with this bundle you should check out 20 | [this blog post](http://developer.happyr.com/how-happyr-work-with-symfony-translations). 21 | 22 | The key features of this bundle is: 23 | 24 | * Easy to download all translations from 25 | * Support for multiple projects 26 | * Create new translation assets by the Symfony WebProfiler 27 | * Edit, flag and synchronize the translation via the Symfony WebProfiler 28 | * Auto upload missing translations to SaaS 29 | 30 | ## Usage 31 | 32 | To download all translations from Loco, simply run: 33 | ``` bash 34 | php app/console happyr:translation:download 35 | ``` 36 | 37 | When you have added new translations you may submit these to your translation SaaS by the WebProfiler toolbar. 38 | 39 | ![New translations to SaaS](src/Resources/doc/images/missing-translation-example.gif) 40 | 41 | You may also change translations and flag them from the same WebProfiler page. 42 | 43 | ![Manage translations with SaaS](src/Resources/doc/images/edit-flag-sync-example.gif) 44 | 45 | When you want to fetch new translations from your SaaS you should run the synchronize command. This command will 46 | keep your current placeholders from missing translations. 47 | 48 | ``` bash 49 | php app/console happyr:translation:sync 50 | ``` 51 | 52 | ## Install 53 | 54 | Install the bundle with `composer require happyr/translation-bundle` 55 | 56 | You do also need to choose what library to use when you are sending http messages. Consult the [php-http/client-implementation](https://packagist.org/providers/php-http/client-implementation) virtual package to find adapters to use. For more information about virtual packages please refer to [Httplug](http://docs.httplug.io/en/latest/virtual-package/). Example: 57 | ```bash 58 | composer require php-http/guzzle6-adapter 59 | ``` 60 | Enable the bundle in your kernel: 61 | 62 | ``` 63 | setName('happyr:translation:download') 21 | ->setDescription('Replace your local files with the latest from your translation SaaS.'); 22 | } 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | protected function execute(InputInterface $input, OutputInterface $output) 27 | { 28 | $this->getContainer()->get('happyr.translation')->downloadAllTranslations(); 29 | $output->writeln('Download complete'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Command/SynchronizeCommand.php: -------------------------------------------------------------------------------- 1 | setName('happyr:translation:sync') 22 | ->setDescription('Sync all your translations with SaaS. Leave place holders for missing translations.'); 23 | } 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected function execute(InputInterface $input, OutputInterface $output) 28 | { 29 | $this->getContainer()->get('happyr.translation')->synchronizeAllTranslations(); 30 | $output->writeln('Synchronization complete'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Command/UploadCommand.php: -------------------------------------------------------------------------------- 1 | setName('happyr:translation:upload') 24 | ->addOption('force', null, InputOption::VALUE_NONE, 'Set this parameter to execute this action') 25 | ->setDescription('Upload your translations into your translator service.'); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | protected function execute(InputInterface $input, OutputInterface $output) 32 | { 33 | if ($input->getOption('force')) { 34 | $this->getContainer()->get('happyr.translation')->uploadAllTranslations(); 35 | $output->writeln('Upload complete'); 36 | } else { 37 | $output->writeln('ATTENTION: This operation should not be executed in a production environment.'); 38 | $output->writeln(''); 39 | $output->writeln(sprintf('This is going to replace every translations on your translator service.')); 40 | $output->writeln('Please run the operation with --force to execute'); 41 | return self::RETURN_CODE_NO_FORCE; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Controller/ProfilerController.php: -------------------------------------------------------------------------------- 1 | getParameter('translation.toolbar.allow_edit')) { 30 | return new Response('You are not allowed to edit the translations.'); 31 | } 32 | 33 | if (!$request->isXmlHttpRequest()) { 34 | return $this->redirectToRoute('_profiler', ['token' => $token]); 35 | } 36 | 37 | $message = $this->getMessage($request, $token); 38 | $trans = $this->get('happyr.translation'); 39 | 40 | if ($request->isMethod('GET')) { 41 | $trans->fetchTranslation($message); 42 | 43 | return $this->render('HappyrTranslationBundle:Profiler:edit.html.twig', [ 44 | 'message' => $message, 45 | 'key' => $request->query->get('message_id'), 46 | ]); 47 | } 48 | 49 | //Assert: This is a POST request 50 | $message->setTranslation($request->request->get('translation')); 51 | $trans->updateTranslation($message); 52 | 53 | return new Response($message->getTranslation()); 54 | } 55 | 56 | /** 57 | * @param Request $request 58 | * @param string $token 59 | * 60 | * @Route("/{token}/translation/flag", name="_profiler_translations_flag") 61 | * @Method("POST") 62 | * 63 | * @return Response 64 | */ 65 | public function flagAction(Request $request, $token) 66 | { 67 | if (!$request->isXmlHttpRequest()) { 68 | return $this->redirectToRoute('_profiler', ['token' => $token]); 69 | } 70 | 71 | $message = $this->getMessage($request, $token); 72 | 73 | $saved = $this->get('happyr.translation')->flagTranslation($message); 74 | 75 | return new Response($saved ? 'OK' : 'ERROR'); 76 | } 77 | 78 | /** 79 | * @param Request $request 80 | * @param string $token 81 | * 82 | * @Route("/{token}/translation/sync", name="_profiler_translations_sync") 83 | * @Method("POST") 84 | * 85 | * @return Response 86 | */ 87 | public function syncAction(Request $request, $token) 88 | { 89 | if (!$request->isXmlHttpRequest()) { 90 | return $this->redirectToRoute('_profiler', ['token' => $token]); 91 | } 92 | 93 | $message = $this->getMessage($request, $token); 94 | $translation = $this->get('happyr.translation')->fetchTranslation($message, true); 95 | 96 | if ($translation !== null) { 97 | return new Response($translation); 98 | } 99 | 100 | return new Response('Asset not found', 404); 101 | } 102 | 103 | /** 104 | * @param Request $request 105 | * @param $token 106 | * 107 | * @Route("/{token}/translation/sync-all", name="_profiler_translations_sync_all") 108 | * 109 | * @return \Symfony\Component\HttpFoundation\RedirectResponse|Response 110 | */ 111 | public function syncAllAction(Request $request, $token) 112 | { 113 | if (!$request->isXmlHttpRequest()) { 114 | return $this->redirectToRoute('_profiler', ['token' => $token]); 115 | } 116 | 117 | $this->get('happyr.translation')->synchronizeAllTranslations(); 118 | 119 | return new Response('Started synchronization of all translations'); 120 | } 121 | 122 | /** 123 | * Save the selected translation to resources. 124 | * 125 | * @author Damien Alexandre (damienalexandre) 126 | * 127 | * @param Request $request 128 | * @param string $token 129 | * 130 | * @Route("/{token}/translation/create-asset", name="_profiler_translations_create_assets") 131 | * @Method("POST") 132 | * 133 | * @return Response 134 | */ 135 | public function createAssetsAction(Request $request, $token) 136 | { 137 | if (!$request->isXmlHttpRequest()) { 138 | return $this->redirectToRoute('_profiler', ['token' => $token]); 139 | } 140 | 141 | $messages = $this->getSelectedMessages($request, $token); 142 | 143 | if ($messages instanceof Data) { 144 | $messages = $messages->getValue(true); 145 | } 146 | 147 | if (empty($messages)) { 148 | return new Response('No translations selected.'); 149 | } 150 | 151 | $uploaded = array(); 152 | $trans = $this->get('happyr.translation'); 153 | foreach ($messages as $message) { 154 | if ($trans->createAsset($message)) { 155 | $uploaded[] = $message; 156 | } 157 | } 158 | 159 | $saved = count($uploaded); 160 | if ($saved > 0) { 161 | $this->get('happyr.translation.filesystem')->updateMessageCatalog($uploaded); 162 | } 163 | 164 | return new Response(sprintf('%s new assets created!', $saved)); 165 | } 166 | 167 | /** 168 | * @param Request $request 169 | * @param string $token 170 | * 171 | * @return Message 172 | */ 173 | protected function getMessage(Request $request, $token) 174 | { 175 | $profiler = $this->get('profiler'); 176 | $profiler->disable(); 177 | 178 | $messageId = $request->request->get('message_id', $request->query->get('message_id')); 179 | 180 | $profile = $profiler->loadProfile($token); 181 | $messages = $profile->getCollector('translation')->getMessages(); 182 | if (!isset($messages[$messageId])) { 183 | throw $this->createNotFoundException(sprintf('No message with key "%s" was found.', $messageId)); 184 | } 185 | $message = new Message($messages[$messageId]); 186 | 187 | if ($message->getState() === DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK) { 188 | $message->setLocale($profile->getCollector('request')->getLocale()) 189 | ->setTranslation(sprintf('[%s]', $message->getTranslation())); 190 | } 191 | 192 | return $message; 193 | } 194 | 195 | /** 196 | * @param Request $request 197 | * @param string $token 198 | * 199 | * @return array 200 | */ 201 | protected function getSelectedMessages(Request $request, $token) 202 | { 203 | $profiler = $this->get('profiler'); 204 | $profiler->disable(); 205 | 206 | $selected = $request->request->get('selected'); 207 | if (!$selected || count($selected) == 0) { 208 | return array(); 209 | } 210 | 211 | $profile = $profiler->loadProfile($token); 212 | $dataCollector = $profile->getCollector('translation'); 213 | $messages = $dataCollector->getMessages(); 214 | 215 | if ($messages instanceof Data) { 216 | $messages = $messages->getValue(true); 217 | } 218 | 219 | $toSave = array_intersect_key($messages, array_flip($selected)); 220 | 221 | $messages = array(); 222 | foreach ($toSave as $data) { 223 | //We do not want do add the placeholder to Loco. That messes up the stats. 224 | $data['translation'] = ''; 225 | 226 | $messages[] = new Message($data); 227 | } 228 | 229 | return $messages; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('happyr_translation'); 20 | 21 | $root->children() 22 | ->enumNode('translation_service')->values(array('blackhole', 'filesystem', 'loco'))->defaultValue('loco')->end() 23 | ->scalarNode('target_dir')->defaultValue('%kernel.root_dir%/Resources/translations')->end() 24 | ->scalarNode('httplug_client')->defaultValue('httplug.client')->cannotBeEmpty()->end() 25 | ->scalarNode('httplug_message_factory')->defaultValue('httplug.message_factory')->cannotBeEmpty()->end() 26 | ->booleanNode('sync_empty_translations')->defaultTrue()->end() 27 | ->booleanNode('auto_add_assets')->defaultFalse()->end() 28 | ->booleanNode('allow_edit')->defaultTrue()->end() 29 | ->enumNode('file_extension')->values(array('csv', 'ini', 'json', 'mo', 'php', 'po', 'qt', 'yml', 'xlf'))->defaultValue('xlf')->end() 30 | ->arrayNode('locales') 31 | ->requiresAtLeastOneElement() 32 | ->prototype('scalar')->end() 33 | ->end() 34 | ->arrayNode('domains') 35 | ->requiresAtLeastOneElement() 36 | ->prototype('scalar')->end() 37 | ->end() 38 | ->append($this->getProjectNode()) 39 | ->end(); 40 | 41 | return $treeBuilder; 42 | } 43 | 44 | /** 45 | * @return \Symfony\Component\Config\Definition\Builder\NodeDefinition 46 | */ 47 | private function getProjectNode() 48 | { 49 | $treeBuilder = new TreeBuilder(); 50 | $node = $treeBuilder->root('projects'); 51 | $node 52 | ->useAttributeAsKey('name') 53 | ->prototype('array') 54 | ->children() 55 | ->scalarNode('api_key')->isRequired()->end() 56 | ->arrayNode('locales') 57 | ->requiresAtLeastOneElement() 58 | ->prototype('scalar')->end() 59 | ->end() 60 | ->arrayNode('domains') 61 | ->requiresAtLeastOneElement() 62 | ->prototype('scalar')->end() 63 | ->end() 64 | ->end() 65 | ->end(); 66 | 67 | return $node; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/DependencyInjection/HappyrTranslationExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 21 | $this->copyValuesFromParentToProject('locales', $config); 22 | $this->copyValuesFromParentToProject('domains', $config); 23 | 24 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 25 | $loader->load('services.yml'); 26 | 27 | if ($config['auto_add_assets']) { 28 | $loader->load('autoAdd.yml'); 29 | } 30 | 31 | $container->setParameter('translation.toolbar.allow_edit', $config['allow_edit']); 32 | 33 | $targetDir = rtrim($config['target_dir'], '/'); 34 | $container->findDefinition('happyr.translation.filesystem') 35 | ->replaceArgument(2, $targetDir) 36 | ->replaceArgument(3, $config['file_extension']) 37 | ->replaceArgument(4, $config['sync_empty_translations']); 38 | 39 | $this->configureLoaderAndDumper($container, $config['file_extension']); 40 | 41 | $container->getDefinition('happyr.translation.request_manager') 42 | ->replaceArgument(0, new Reference($config['httplug_client'])) 43 | ->replaceArgument(1, new Reference($config['httplug_message_factory'])); 44 | 45 | /* 46 | * Set alias for the translation service 47 | */ 48 | $container->setAlias('happyr.translation', 'happyr.translation.service.'.$config['translation_service']); 49 | 50 | $container->findDefinition('happyr.translation.service.loco') 51 | ->replaceArgument(3, $config['projects']); 52 | } 53 | 54 | /** 55 | * Copy the parent configuration to the children. 56 | * 57 | * @param string $key 58 | * @param array $config 59 | */ 60 | private function copyValuesFromParentToProject($key, array &$config) 61 | { 62 | if (empty($config[$key])) { 63 | return; 64 | } 65 | 66 | foreach ($config['projects'] as &$project) { 67 | if (empty($project[$key])) { 68 | $project[$key] = $config[$key]; 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * @param ContainerBuilder $container 75 | * @param string $fileExtension 76 | */ 77 | protected function configureLoaderAndDumper(ContainerBuilder $container, $fileExtension) 78 | { 79 | switch ($fileExtension) { 80 | case 'xlf': 81 | $fileExtension = 'xliff'; 82 | break; 83 | case 'yml': 84 | $fileExtension = 'yaml'; 85 | break; 86 | } 87 | 88 | $loader = $container->register('happyr.translation.loader', sprintf('Symfony\Component\Translation\Loader\%sFileLoader', ucfirst($fileExtension))); 89 | $loader->addTag('translation.loader', ['alias' => $fileExtension]); 90 | 91 | $dumper = $container->register('happyr.translation.dumper', sprintf('Symfony\Component\Translation\Dumper\%sFileDumper', ucfirst($fileExtension))); 92 | $dumper->addTag('translation.dumper', ['alias' => $fileExtension]); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Exception/HappyrTranslationException.php: -------------------------------------------------------------------------------- 1 | client = new PluginClient($client, [new ErrorPlugin()]); 37 | $this->messageFactory = $messageFactory; 38 | } 39 | 40 | /** 41 | * @param array $data 42 | * 43 | * @return mixed 44 | */ 45 | public function downloadFiles(FilesystemUpdater $filesystem, array $data) 46 | { 47 | $factory = $this->getMessageFactory(); 48 | $client = $this->getClient(); 49 | 50 | foreach ($data as $url => $fileName) { 51 | $response = $client->sendRequest($factory->createRequest('GET', $url)); 52 | $filesystem->writeToFile($fileName, $response->getBody()->__toString()); 53 | } 54 | } 55 | 56 | /** 57 | * @param string $method 58 | * @param string $url 59 | * @param array $data 60 | * 61 | * @return array 62 | * 63 | * @throws HttpException 64 | */ 65 | public function send($method, $url, $body = null, $headers = array()) 66 | { 67 | $request = $this->getMessageFactory()->createRequest($method, $url, $headers, $body); 68 | 69 | try { 70 | $response = $this->getClient()->sendRequest($request); 71 | } catch (TransferException $e) { 72 | $message = 'Error sending request. '; 73 | if ($e instanceof \Http\Client\Exception\HttpException) { 74 | $message .= (string) $e->getResponse()->getBody(); 75 | } 76 | 77 | throw new HttpException($message, $e->getCode(), $e); 78 | } 79 | 80 | // TODO add more error checks 81 | return json_decode($response->getBody()->__toString(), true); 82 | } 83 | 84 | /** 85 | * @return HttpClient 86 | */ 87 | public function getClient() 88 | { 89 | return $this->client; 90 | } 91 | 92 | /** 93 | * 94 | * @return \Http\Message\MessageFactory 95 | */ 96 | private function getMessageFactory() 97 | { 98 | return $this->messageFactory; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Model/Message.php: -------------------------------------------------------------------------------- 1 | 2.8 61 | */ 62 | private $transChoiceNumber; 63 | 64 | /** 65 | * @var array 66 | * 67 | * The parameters sent to the translations 68 | * Used only in Symfony >2.8 69 | */ 70 | private $parameters; 71 | 72 | /** 73 | * @param array $data 74 | * array( count = 1, domain = "navigation", id = "logout", locale = "sv", state = 1, translation = "logout" ) 75 | */ 76 | public function __construct($data) 77 | { 78 | $this->domain = $data['domain']; 79 | $this->id = $data['id']; 80 | $this->locale = $data['locale']; 81 | $this->state = $data['state']; 82 | $this->translation = $data['translation']; 83 | 84 | if (isset($data['count'])) { 85 | $this->count = $data['count']; 86 | } 87 | 88 | if (isset($data['transChoiceNumber'])) { 89 | $this->transChoiceNumber = $data['transChoiceNumber']; 90 | } 91 | 92 | if (isset($data['parameters'])) { 93 | $this->parameters = $data['parameters']; 94 | } 95 | } 96 | 97 | /** 98 | * @return int 99 | */ 100 | public function getCount() 101 | { 102 | return $this->count; 103 | } 104 | 105 | /** 106 | * @param int $count 107 | * 108 | * @return $this 109 | */ 110 | public function setCount($count) 111 | { 112 | $this->count = $count; 113 | 114 | return $this; 115 | } 116 | 117 | /** 118 | * @return string 119 | */ 120 | public function getDomain() 121 | { 122 | return $this->domain; 123 | } 124 | 125 | /** 126 | * @param string $domain 127 | * 128 | * @return $this 129 | */ 130 | public function setDomain($domain) 131 | { 132 | $this->domain = $domain; 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * @return string 139 | */ 140 | public function getId() 141 | { 142 | return $this->id; 143 | } 144 | 145 | /** 146 | * @param string $id 147 | * 148 | * @return $this 149 | */ 150 | public function setId($id) 151 | { 152 | $this->id = $id; 153 | 154 | return $this; 155 | } 156 | 157 | /** 158 | * @return string 159 | */ 160 | public function getLocale() 161 | { 162 | return $this->locale; 163 | } 164 | 165 | /** 166 | * @param string $locale 167 | * 168 | * @return $this 169 | */ 170 | public function setLocale($locale) 171 | { 172 | $this->locale = $locale; 173 | 174 | return $this; 175 | } 176 | 177 | /** 178 | * @return int 179 | */ 180 | public function getState() 181 | { 182 | return $this->state; 183 | } 184 | 185 | /** 186 | * @param int $state 187 | * 188 | * @return $this 189 | */ 190 | public function setState($state) 191 | { 192 | $this->state = $state; 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * @return string 199 | */ 200 | public function getTranslation() 201 | { 202 | return $this->translation; 203 | } 204 | 205 | /** 206 | * @param string $translation 207 | * 208 | * @return $this 209 | */ 210 | public function setTranslation($translation) 211 | { 212 | $this->translation = $translation; 213 | 214 | return $this; 215 | } 216 | 217 | /** 218 | * @return int 219 | */ 220 | public function getTransChoiceNumber() 221 | { 222 | return $this->transChoiceNumber; 223 | } 224 | 225 | /** 226 | * @param int $transChoiceNumber 227 | * 228 | * @return $this 229 | */ 230 | public function setTransChoiceNumber($transChoiceNumber) 231 | { 232 | $this->transChoiceNumber = $transChoiceNumber; 233 | 234 | return $this; 235 | } 236 | 237 | /** 238 | * @return array 239 | */ 240 | public function getParameters() 241 | { 242 | return $this->parameters; 243 | } 244 | 245 | /** 246 | * @return bool 247 | */ 248 | public function hasParameters() 249 | { 250 | return !empty($this->parameters); 251 | } 252 | 253 | /** 254 | * @param array $parameters 255 | * 256 | * @return $this 257 | */ 258 | public function setParameters($parameters) 259 | { 260 | $this->parameters = $parameters; 261 | 262 | return $this; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/Resources/config/autoAdd.yml: -------------------------------------------------------------------------------- 1 | 2 | 3 | services: 4 | happyr.translation.auto_adder: 5 | class: Happyr\TranslationBundle\Translation\AutoAdder 6 | arguments: [ '@?translator.data_collector', '@happyr.translation', '@happyr.translation.filesystem' ] 7 | tags: 8 | - { name: kernel.event_listener, event: kernel.terminate, method: onTerminate, priority: 10 } 9 | -------------------------------------------------------------------------------- /src/Resources/config/routing_dev.yml: -------------------------------------------------------------------------------- 1 | _logo_translation_profiler: 2 | resource: "@HappyrTranslationBundle/Controller/ProfilerController.php" 3 | type: annotation 4 | prefix: /_profiler 5 | -------------------------------------------------------------------------------- /src/Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | 3 | services: 4 | 5 | # Custom data_collector to use our own template 6 | happyr.translation.data_collector: 7 | class: Symfony\Component\Translation\DataCollector\TranslationDataCollector 8 | arguments: [ '@?translator.data_collector' ] 9 | tags: 10 | - { name: 'data_collector', template: "@HappyrTranslation/Profiler/translation", id: "translation", priority: 200 } 11 | 12 | happyr.translation.service.blackhole: 13 | class: Happyr\TranslationBundle\Service\Blackhole 14 | 15 | happyr.translation.service.filesystem: 16 | class: Happyr\TranslationBundle\Service\Filesystem 17 | arguments: ['@happyr.translation.filesystem'] 18 | 19 | happyr.translation.filesystem: 20 | class: Happyr\TranslationBundle\Translation\FilesystemUpdater 21 | arguments: 22 | - '@happyr.translation.loader' 23 | - '@happyr.translation.dumper' 24 | - ~ 25 | - ~ 26 | - ~ 27 | tags: 28 | - { name: kernel.event_listener, event: kernel.terminate, method: onTerminate, priority: -20 } 29 | - { name: kernel.event_listener, event: console.terminate, method: onTerminate, priority: -20 } 30 | 31 | happyr.translation.service.loco: 32 | class: Happyr\TranslationBundle\Service\Loco 33 | arguments: 34 | - '@happyr.translation.request_manager' 35 | - '@happyr.translation.filesystem' 36 | - '@translator' 37 | - ~ 38 | 39 | happyr.translation.request_manager: 40 | class: Happyr\TranslationBundle\Http\RequestManager 41 | arguments: [~, ~] 42 | public: false 43 | -------------------------------------------------------------------------------- /src/Resources/doc/images/edit-flag-sync-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happyr/TranslationBundle/497c042a1b89434b0b874e32c80cec2f5a08cc17/src/Resources/doc/images/edit-flag-sync-example.gif -------------------------------------------------------------------------------- /src/Resources/doc/images/missing-translation-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happyr/TranslationBundle/497c042a1b89434b0b874e32c80cec2f5a08cc17/src/Resources/doc/images/missing-translation-example.gif -------------------------------------------------------------------------------- /src/Resources/doc/images/toolbar-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happyr/TranslationBundle/497c042a1b89434b0b874e32c80cec2f5a08cc17/src/Resources/doc/images/toolbar-example.png -------------------------------------------------------------------------------- /src/Resources/views/Profiler/edit.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Resources/views/Profiler/javascripts.html.twig: -------------------------------------------------------------------------------- 1 | 176 | -------------------------------------------------------------------------------- /src/Resources/views/Profiler/translation.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Collector/translation.html.twig' %} 2 | 3 | {# 4 | # Authors Tobias Nyholm, Damien Alexandre (damienalexandre), Damien Harper 5 | #} 6 | 7 | {% import _self as translation_helper %} 8 | 9 | {% block panelContent %} 10 |

Translation Metrics

11 | 12 |
13 | Synchronize all translations
14 |
15 |
16 | 17 |
18 |
19 | {{ collector.countdefines }} 20 | Defined messages 21 |
22 | 23 |
24 | {{ collector.countFallbacks }} 25 | Fallback messages 26 |
27 | 28 |
29 | {{ collector.countMissings }} 30 | Missing messages 31 |
32 |
33 | 34 |

Translation Messages

35 | 36 | {# sort translation messages in groups #} 37 | {% set messages_defined, messages_missing, messages_fallback = [], [], [] %} 38 | {% for key, message in collector.messages %} 39 | {% if message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_DEFINED') %} 40 | {% set messages_defined = messages_defined|merge({(key): message}) %} 41 | {% elseif message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_MISSING') %} 42 | {% set messages_missing = messages_missing|merge({(key): message}) %} 43 | {% elseif message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK') %} 44 | {% set messages_fallback = messages_fallback|merge({(key): message}) %} 45 | {% endif %} 46 | {% endfor %} 47 | 48 |
50 | 51 |
52 |
53 |

Defined {{ messages_defined|length }}

54 | 55 |
56 |

57 | These messages are correctly translated into the given locale. 58 |

59 | 60 | {% if messages_defined is empty %} 61 |
62 |

None of the used translation messages are defined for the given locale.

63 |
64 | {% else %} 65 | {{ translation_helper.render_table(messages_defined) }} 66 | {% endif %} 67 |
68 |
69 | 70 |
71 |

Fallback {{ messages_fallback|length }}

72 | 73 |
74 |

75 | These messages are not available for the given locale 76 | but Symfony found them in the fallback locale catalog. 77 |

78 | 79 | {% if messages_fallback is empty %} 80 |
81 |

No fallback translation messages were used.

82 |
83 | {% else %} 84 | {{ translation_helper.render_table(messages_fallback) }} 85 | {% endif %} 86 |
87 |
88 | 89 |
90 |

Missing {{ messages_missing|length }}

91 | 92 |
93 |

94 | These messages are not available for the given locale and cannot 95 | be found in the fallback locales. Add them to the translation 96 | catalogue to avoid Symfony outputting untranslated contents. 97 |

98 | 99 | {% if messages_missing is empty %} 100 |
101 |

There are no messages of this category.

102 |
103 | {% else %} 104 | {{ translation_helper.render_table(messages_missing) }} 105 | {% endif %} 106 |
107 |
108 |
109 | 110 | 113 | 114 |
115 | 116 |
117 | {% include "HappyrTranslationBundle:Profiler:javascripts.html.twig" %} 118 | {% endblock %} 119 | 120 | {% macro render_table(messages) %} 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | {% for key, message in messages %} 135 | 136 | 143 | 144 | 145 | 146 | 166 | 167 | 176 | 177 | {% endfor %} 178 | 179 |
LocaleDomainTimes usedMessage IDMessage PreviewActions
137 | {% if message.state == constant('Symfony\\Component\\Translation\\DataCollectorTranslator::MESSAGE_MISSING') %} 138 | 139 | {% else %} 140 | 141 | {% endif %} 142 | {{ message.locale }}{{ message.domain }}{{ message.count }} 147 | {{ message.id }} 148 | 149 | {# message.transChoiceNumber is not defined before symfony 2.8.0 #} 150 | {% if message.transChoiceNumber is defined and message.transChoiceNumber is not null %} 151 | (pluralization is used) 152 | {% endif %} 153 | 154 | {# message.parameters is not defined before symfony 2.8.0 #} 155 | {% if message.parameters is defined and message.parameters|length > 0 %} 156 | 157 | 158 | 164 | {% endif %} 165 | {{ message.translation }} 168 | {% spaceless %} 169 | Edit 170 | | 171 | Flag 172 | | 173 | Sync 174 | {% endspaceless %} 175 |
180 | {% endmacro %} 181 | -------------------------------------------------------------------------------- /src/Service/Blackhole.php: -------------------------------------------------------------------------------- 1 | filesystemService = $filesystemService; 22 | } 23 | 24 | /** 25 | * @inheritDoc 26 | */ 27 | public function fetchTranslation(Message $message, $updateFs = false) 28 | { 29 | if ($updateFs) { 30 | $this->filesystemService->updateMessageCatalog([$message]); 31 | } 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | public function updateTranslation(Message $message) 38 | { 39 | $this->filesystemService->updateMessageCatalog([$message]); 40 | } 41 | 42 | /** 43 | * @inheritDoc 44 | */ 45 | public function flagTranslation(Message $message, $type = 0) 46 | { 47 | } 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | public function createAsset(Message $message) 53 | { 54 | } 55 | 56 | /** 57 | * @inheritDoc 58 | */ 59 | public function downloadAllTranslations() 60 | { 61 | } 62 | 63 | /** 64 | * @inheritDoc 65 | */ 66 | public function importAllTranslations() 67 | { 68 | } 69 | 70 | /** 71 | * @inheritDoc 72 | */ 73 | public function synchronizeAllTranslations() 74 | { 75 | } 76 | 77 | /** 78 | * @inheritDoc 79 | */ 80 | public function uploadAllTranslations() 81 | { 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Service/Loco.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class Loco implements TranslationServiceInterface 17 | { 18 | const BASE_URL = 'https://localise.biz/api/'; 19 | 20 | /** 21 | * @var RequestManager 22 | */ 23 | private $requestManager; 24 | 25 | /** 26 | * @var array projects 27 | */ 28 | private $projects; 29 | 30 | /** 31 | * @var FilesystemUpdater filesystemService 32 | */ 33 | private $filesystemService; 34 | 35 | /** 36 | * @var TranslatorInterface filesystemService 37 | */ 38 | private $translator; 39 | 40 | /** 41 | * @param TranslatorInterface $translator 42 | * @param RequestManager $requestManager 43 | * @param FilesystemUpdater $fs 44 | * @param array $projects 45 | */ 46 | public function __construct(RequestManager $requestManager, FilesystemUpdater $fs, TranslatorInterface $translator, array $projects) 47 | { 48 | $this->translator = $translator; 49 | $this->requestManager = $requestManager; 50 | $this->projects = $projects; 51 | $this->filesystemService = $fs; 52 | } 53 | 54 | /** 55 | * @param $key 56 | * @param $method 57 | * @param $resource 58 | * @param null $body 59 | * @param string $type 60 | * @param array $extraQuery 61 | * @return array 62 | * @throws HttpException 63 | */ 64 | protected function makeApiRequest($key, $method, $resource, $body = null, $type = 'form', $extraQuery = array()) 65 | { 66 | $headers = array(); 67 | if ($body !== null) { 68 | if ($type === 'form') { 69 | if (is_array($body)) { 70 | $body = http_build_query($body); 71 | } 72 | $headers['Content-Type'] = 'application/x-www-form-urlencoded'; 73 | } elseif ($type === 'json') { 74 | $body = json_encode($body); 75 | $headers['Content-Type'] = 'application/json'; 76 | } 77 | } 78 | 79 | $query = array_merge($extraQuery, ['key' => $key]); 80 | $url = self::BASE_URL . $resource . '?' . http_build_query($query); 81 | 82 | return $this->requestManager->send($method, $url, $body, $headers); 83 | } 84 | 85 | /** 86 | * Fetch a translation form Loco. 87 | * 88 | * @param Message $message 89 | */ 90 | public function fetchTranslation(Message $message, $updateFs = false) 91 | { 92 | $project = $this->getProject($message); 93 | 94 | try { 95 | $resource = sprintf('translations/%s/%s', $message->getId(), $message->getLocale()); 96 | $response = $this->makeApiRequest($project['api_key'], 'GET', $resource); 97 | } catch (HttpException $e) { 98 | if ($e->getCode() === 404) { 99 | //Message does not exist 100 | return; 101 | } 102 | throw $e; 103 | } 104 | 105 | $logoTranslation = $response['translation']; 106 | $messageTranslation = $message->getTranslation(); 107 | $message->setTranslation($logoTranslation); 108 | 109 | // update filesystem 110 | if ($updateFs && $logoTranslation !== $messageTranslation) { 111 | $this->filesystemService->updateMessageCatalog([$message]); 112 | } 113 | 114 | return $logoTranslation; 115 | } 116 | 117 | /** 118 | * Update the translation in Loco. 119 | * 120 | * @param Message $message 121 | */ 122 | public function updateTranslation(Message $message) 123 | { 124 | $project = $this->getProject($message); 125 | 126 | try { 127 | $resource = sprintf('translations/%s/%s', $message->getId(), $message->getLocale()); 128 | $this->makeApiRequest($project['api_key'], 'POST', $resource, $message->getTranslation()); 129 | } catch (HttpException $e) { 130 | if ($e->getCode() === 404) { 131 | //Asset does not exist 132 | if ($this->createAsset($message)) { 133 | //Try again 134 | return $this->updateTranslation($message); 135 | } 136 | 137 | return false; 138 | } 139 | throw $e; 140 | } 141 | 142 | $this->filesystemService->updateMessageCatalog([$message]); 143 | 144 | return true; 145 | } 146 | 147 | /** 148 | * If there is something wrong with the translation, please flag it. 149 | * 150 | * @param Message $message 151 | * @param int $type 0: Fuzzy, 1: Incorrect, 2: Provisional, 3: Unapproved, 4: Incomplete 152 | * 153 | * @return bool 154 | */ 155 | public function flagTranslation(Message $message, $type = 0) 156 | { 157 | $project = $this->getProject($message); 158 | $flags = ['fuzzy', 'incorrect', 'provisional', 'unapproved', 'incomplete']; 159 | 160 | try { 161 | $resource = sprintf('translations/%s/%s/flag', $message->getId(), $message->getLocale()); 162 | $this->makeApiRequest($project['api_key'], 'POST', $resource, ['flag' => $flags[$type]]); 163 | } catch (HttpException $e) { 164 | if ($e->getCode() === 404) { 165 | //Message does not exist 166 | return false; 167 | } 168 | throw $e; 169 | } 170 | 171 | return true; 172 | } 173 | 174 | /** 175 | * Create a new asset in Loco. 176 | * 177 | * @param Message $message 178 | * 179 | * @return bool 180 | */ 181 | public function createAsset(Message $message) 182 | { 183 | $project = $this->getProject($message); 184 | 185 | try { 186 | $response = $this->makeApiRequest($project['api_key'], 'POST', 'assets', [ 187 | 'id' => $message->getId(), 188 | 'name' => $message->getId(), 189 | 'type' => 'text', 190 | // Tell Loco not to translate the asset 191 | 'default' => 'untranslated', 192 | ]); 193 | 194 | if ($message->hasParameters()) { 195 | // Send those parameter as a note to Loco 196 | $notes = ''; 197 | foreach ($message->getParameters() as $key => $value) { 198 | if (!is_array($value)) { 199 | $notes .= 'Parameter: ' . $key . ' (i.e. : ' . $value . ")\n"; 200 | } else { 201 | foreach ($value as $k => $v) { 202 | $notes .= 'Parameter: ' . $k . ' (i.e. : ' . $v . ")\n"; 203 | } 204 | } 205 | } 206 | 207 | $resource = sprintf('assets/%s.json', $message->getId()); 208 | $this->makeApiRequest($project['api_key'], 'PATCH', $resource, ['notes' => $notes], 'json'); 209 | } 210 | } catch (HttpException $e) { 211 | if ($e->getCode() === 409) { 212 | //conflict.. ignore 213 | return false; 214 | } 215 | throw $e; 216 | } 217 | 218 | // if this project has multiple domains. Make sure to tag it 219 | if (!empty($project['domains'])) { 220 | $this->addTagToAsset($project, $response['id'], $message->getDomain()); 221 | } 222 | 223 | return true; 224 | } 225 | 226 | /** 227 | * @param Message $message 228 | * 229 | * @return array 230 | */ 231 | protected function getProject(Message $message) 232 | { 233 | if (isset($this->projects[$message->getDomain()])) { 234 | return $this->projects[$message->getDomain()]; 235 | } 236 | 237 | // Return the first project that has the correct domain and locale 238 | foreach ($this->projects as $project) { 239 | if (in_array($message->getDomain(), $project['domains'])) { 240 | if (in_array($message->getLocale(), $project['locales'])) { 241 | return $project; 242 | } 243 | } 244 | } 245 | } 246 | 247 | /** 248 | * @param $project 249 | * @param $messageId 250 | * @param $domain 251 | */ 252 | protected function addTagToAsset($project, $messageId, $domain) 253 | { 254 | $resource = sprintf('assets/%s/tags', $messageId); 255 | $this->makeApiRequest($project['api_key'], 'POST', $resource, ['name' => $domain]); 256 | } 257 | 258 | /** 259 | * Download all the translations from Loco. This will replace all the local files. 260 | * This is a quick method of getting all the latest translations and assets. 261 | */ 262 | public function downloadAllTranslations() 263 | { 264 | $data = []; 265 | foreach ($this->projects as $name => $config) { 266 | if (empty($config['domains'])) { 267 | $this->getUrls($data, $config, $name, false); 268 | } else { 269 | foreach ($config['domains'] as $domain) { 270 | $this->getUrls($data, $config, $domain, true); 271 | } 272 | } 273 | } 274 | $this->requestManager->downloadFiles($this->filesystemService, $data); 275 | } 276 | 277 | /** 278 | * Upload all the translations from the symfony project into Loco. This will override 279 | * every changed strings in loco 280 | */ 281 | public function uploadAllTranslations() 282 | { 283 | foreach ($this->projects as $name => $config) { 284 | if (empty($config['domains'])) { 285 | $this->doUploadDomains($config, $name, false); 286 | } else { 287 | foreach ($config['domains'] as $domain) { 288 | $this->doUploadDomains($config, $domain, true); 289 | } 290 | } 291 | } 292 | } 293 | 294 | /** 295 | * @param array $config 296 | * @param $domain 297 | * @param $useDomainAsFilter 298 | */ 299 | protected function doUploadDomains(array &$config, $domain, $useDomainAsFilter) 300 | { 301 | $query = $this->getExportQueryParams($config['api_key']); 302 | 303 | if ($useDomainAsFilter) { 304 | $query['filter'] = $domain; 305 | } 306 | 307 | foreach ($config['locales'] as $locale) { 308 | $extension = $this->filesystemService->getFileExtension(); 309 | $file = $this->filesystemService->getTargetDir(); 310 | $file .= sprintf('/%s.%s.%s', $domain, $locale, $extension); 311 | 312 | if (is_file($file)) { 313 | $query = [ 314 | 'index' => 'id', 315 | 'tag' => $domain, 316 | 'locale'=> $locale 317 | ]; 318 | 319 | $resource = sprintf('import/%s', $extension); 320 | $response = $this->makeApiRequest($config['api_key'], 'POST', $resource, file_get_contents($file), 'form', $query); 321 | $this->flatten($response); 322 | } else { 323 | throw new FileNotFoundException(sprintf("Can't find %s file, perhaps you should generate the translations file ?", $file)); 324 | } 325 | } 326 | } 327 | 328 | /** 329 | * Synchronize all the translations with Loco. This will keep placeholders. This function is slower 330 | * than just to download the translations. 331 | */ 332 | public function synchronizeAllTranslations() 333 | { 334 | foreach ($this->projects as $name => $config) { 335 | if (empty($config['domains'])) { 336 | $this->doSynchronizeDomain($config, $name, false); 337 | } else { 338 | foreach ($config['domains'] as $domain) { 339 | $this->doSynchronizeDomain($config, $domain, true); 340 | } 341 | } 342 | } 343 | } 344 | 345 | /** 346 | * @param array $config 347 | * @param $domain 348 | * @param $useDomainAsFilter 349 | */ 350 | protected function doSynchronizeDomain(array &$config, $domain, $useDomainAsFilter) 351 | { 352 | $query = $this->getExportQueryParams($config['api_key']); 353 | 354 | if ($useDomainAsFilter) { 355 | $query['filter'] = $domain; 356 | } 357 | 358 | foreach ($config['locales'] as $locale) { 359 | $resource = sprintf('export/locale/%s.%s', $locale, 'json'); 360 | $response = $this->makeApiRequest($config['api_key'], 'GET', $resource, ['query' => $query]); 361 | 362 | $this->flatten($response); 363 | 364 | $messages = array(); 365 | foreach ($response as $id => $translation) { 366 | $messages[] = new Message([ 367 | 'count' => 1, 368 | 'domain' => $domain, 369 | 'id' => $id, 370 | 'locale' => $locale, 371 | 'state' => 1, 372 | 'translation' => $translation, 373 | ]); 374 | } 375 | 376 | $this->filesystemService->updateMessageCatalog($messages); 377 | } 378 | } 379 | 380 | /** 381 | * Flattens an nested array of translations. 382 | * 383 | * The scheme used is: 384 | * 'key' => array('key2' => array('key3' => 'value')) 385 | * Becomes: 386 | * 'key.key2.key3' => 'value' 387 | * 388 | * This function takes an array by reference and will modify it 389 | * 390 | * @param array &$messages The array that will be flattened 391 | * @param array $subnode Current subnode being parsed, used internally for recursive calls 392 | * @param string $path Current path being parsed, used internally for recursive calls 393 | */ 394 | private function flatten(array &$messages, array $subnode = null, $path = null) 395 | { 396 | if (null === $subnode) { 397 | $subnode = &$messages; 398 | } 399 | foreach ($subnode as $key => $value) { 400 | if (is_array($value)) { 401 | $nodePath = $path ? $path . '.' . $key : $key; 402 | $this->flatten($messages, $value, $nodePath); 403 | if (null === $path) { 404 | unset($messages[$key]); 405 | } 406 | } elseif (null !== $path) { 407 | $messages[$path . '.' . $key] = $value; 408 | } 409 | } 410 | } 411 | 412 | /** 413 | * @param array $data 414 | * @param array $config 415 | * @param string $domain 416 | * @param bool $useDomainAsFilter 417 | */ 418 | protected function getUrls(array &$data, array $config, $domain, $useDomainAsFilter) 419 | { 420 | $query = $this->getExportQueryParams($config['api_key']); 421 | 422 | if ($useDomainAsFilter) { 423 | $query['filter'] = $domain; 424 | } 425 | 426 | foreach ($config['locales'] as $locale) { 427 | // Build url 428 | $url = sprintf('%sexport/locale/%s.%s?%s', self::BASE_URL, $locale, $this->filesystemService->getFileExtension(), http_build_query($query)); 429 | $fileName = sprintf('%s.%s.%s', $domain, $locale, $this->filesystemService->getFileExtension()); 430 | 431 | $data[$url] = $fileName; 432 | } 433 | } 434 | 435 | /** 436 | * @param array $config 437 | * 438 | * @return array 439 | */ 440 | private function getExportQueryParams($key) 441 | { 442 | $data = array( 443 | 'index' => 'id', 444 | 'status' => 'translated', 445 | 'key' => $key, 446 | ); 447 | switch ($this->filesystemService->getFileExtension()) { 448 | case 'php': 449 | $data['format'] = 'zend'; // 'Zend' will give us a flat array 450 | break; 451 | case 'xlf': 452 | default: 453 | $data['format'] = 'symfony'; 454 | } 455 | 456 | return $data; 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /src/Service/TranslationServiceInterface.php: -------------------------------------------------------------------------------- 1 | translator = $translator; 33 | $this->transService = $transService; 34 | $this->fileSystemUpdater = $fileSystemUpdater; 35 | } 36 | 37 | public function onTerminate(Event $event) 38 | { 39 | if ($this->translator === null) { 40 | return; 41 | } 42 | 43 | $messages = $this->translator->getCollectedMessages(); 44 | $created = array(); 45 | foreach ($messages as $message) { 46 | if ($message['state'] === DataCollectorTranslator::MESSAGE_MISSING) { 47 | $m = new Message($message); 48 | $this->transService->createAsset($m); 49 | $created[] = $m; 50 | } 51 | } 52 | 53 | if (count($created) > 0) { 54 | // update filesystem 55 | $this->fileSystemUpdater->updateMessageCatalog($created); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Translation/FilesystemUpdater.php: -------------------------------------------------------------------------------- 1 | loader = $loader; 61 | $this->dumper = $dumper; 62 | $this->targetDir = $targetDir; 63 | $this->messages = array(); 64 | $this->fileExtension = $fileExtension; 65 | $this->syncEmptyTranslations = $syncEmptyTranslations; 66 | } 67 | 68 | /** 69 | * Returns translation file type. 70 | * 71 | * @return string 72 | */ 73 | public function getFileExtension() 74 | { 75 | return $this->fileExtension; 76 | } 77 | 78 | /** 79 | * @param string $fileName 80 | * @param string $data the file contents 81 | */ 82 | public function writeToFile($fileName, $data) 83 | { 84 | if (!is_dir($this->targetDir)) { 85 | mkdir($this->targetDir, 0777, true); 86 | } 87 | 88 | file_put_contents(sprintf('%s/%s', $this->targetDir, $fileName), $data); 89 | } 90 | 91 | /** 92 | * Update message catalogues. 93 | * 94 | * @param Message[] $messages 95 | */ 96 | public function updateMessageCatalog(array $messages) 97 | { 98 | $this->messages = array_merge($messages, $this->messages); 99 | } 100 | 101 | /** 102 | * Update the file system after the Response has been sent back to the client. 103 | * 104 | * @param Event $event 105 | * 106 | * @throws \ErrorException 107 | * @throws \Exception 108 | */ 109 | public function onTerminate(Event $event) 110 | { 111 | if (empty($this->messages)) { 112 | return; 113 | } 114 | 115 | /** @var MessageCatalogue[] $catalogues */ 116 | $catalogues = array(); 117 | foreach ($this->messages as $m) { 118 | $key = $m->getLocale().$m->getDomain(); 119 | if (!isset($catalogues[$key])) { 120 | $file = sprintf('%s/%s.%s.%s', $this->targetDir, $m->getDomain(), $m->getLocale(), $this->getFileExtension()); 121 | 122 | try { 123 | $catalogues[$key] = $this->loader->load($file, $m->getLocale(), $m->getDomain()); 124 | } catch (NotFoundResourceException $e) { 125 | $catalogues[$key] = new MessageCatalogue($m->getLocale()); 126 | } 127 | } 128 | 129 | $translation = $m->getTranslation(); 130 | if (empty($translation)) { 131 | if ($this->syncEmptyTranslations) { 132 | $translation = sprintf('[%s]', $m->getId()); 133 | } else { 134 | continue; 135 | } 136 | } 137 | 138 | $catalogues[$key]->set($m->getId(), $translation, $m->getDomain()); 139 | } 140 | 141 | foreach ($catalogues as $catalogue) { 142 | try { 143 | $this->dumper->dump($catalogue, ['path' => $this->targetDir]); 144 | } catch (\ErrorException $e) { 145 | // Could not save file 146 | // TODO better error handling 147 | throw $e; 148 | } 149 | } 150 | } 151 | 152 | /** 153 | * @return string 154 | */ 155 | public function getTargetDir() 156 | { 157 | return $this->targetDir; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /tests/Functional/AppKernel.php: -------------------------------------------------------------------------------- 1 | isAbsolutePath($config)) { 20 | $config = __DIR__.'/config/'.$config; 21 | } 22 | 23 | if (!file_exists($config)) { 24 | throw new \RuntimeException(sprintf('The config file "%s" does not exist.', $config)); 25 | } 26 | 27 | $this->config = $config; 28 | } 29 | 30 | public function registerBundles() 31 | { 32 | return array( 33 | new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(), 34 | new \Symfony\Bundle\SecurityBundle\SecurityBundle(), 35 | new \Happyr\TranslationBundle\HappyrTranslationBundle(), 36 | new \Http\HttplugBundle\HttplugBundle(), 37 | ); 38 | } 39 | 40 | public function registerContainerConfiguration(LoaderInterface $loader) 41 | { 42 | $loader->load($this->config); 43 | } 44 | 45 | public function getCacheDir() 46 | { 47 | return sys_get_temp_dir().'/HappyrTranslationBundle'; 48 | } 49 | 50 | public function serialize() 51 | { 52 | return $this->config; 53 | } 54 | 55 | public function unserialize($config) 56 | { 57 | $this->__construct($config); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Functional/BaseTestCase.php: -------------------------------------------------------------------------------- 1 | getContainer(); 15 | $container->get('happyr.translation.service.loco'); 16 | $container->get('happyr.translation.service.blackhole'); 17 | $container->get('happyr.translation.service.filesystem'); 18 | } 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /tests/Functional/config/default.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: framework.yml } 3 | 4 | -------------------------------------------------------------------------------- /tests/Functional/config/framework.yml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: test 3 | test: ~ 4 | session: 5 | storage_id: session.storage.mock_file 6 | form: false 7 | csrf_protection: false 8 | validation: 9 | enabled: false 10 | router: 11 | resource: "%kernel.root_dir%/config/routing.yml" 12 | 13 | httplug: 14 | classes: 15 | client: Http\Adapter\Guzzle6\Client 16 | message_factory: Http\Message\MessageFactory\GuzzleMessageFactory 17 | uri_factory: Http\Message\UriFactory\GuzzleUriFactory 18 | stream_factory: Http\Message\StreamFactory\GuzzleStreamFactory -------------------------------------------------------------------------------- /tests/Functional/config/routing.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happyr/TranslationBundle/497c042a1b89434b0b874e32c80cec2f5a08cc17/tests/Functional/config/routing.yml --------------------------------------------------------------------------------