├── .php-cs-fixer.php ├── Changelog.md ├── LICENSE ├── Makefile ├── Readme.md ├── composer.json ├── phpstan-baseline.neon ├── phpstan.neon.dist ├── src ├── Bridge │ └── Symfony │ │ ├── DependencyInjection │ │ ├── Configuration.php │ │ └── TranslationAdapterLocoExtension.php │ │ └── TranslationAdapterLocoBundle.php ├── Loco.php └── Model │ └── LocoProject.php └── tests ├── .gitkeep ├── Functional └── BundleInitializationTest.php └── Unit ├── LocoTest.php └── Model └── LocoProjectTest.php /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | name('*.php') 5 | ->in(__DIR__.'/src') 6 | ->in(__DIR__.'/tests') 7 | ; 8 | 9 | $config = new PhpCsFixer\Config(); 10 | 11 | return $config->setRules([ 12 | '@Symfony' => true, 13 | '@Symfony:risky' => true, 14 | ]) 15 | ->setRiskyAllowed(true) 16 | ->setFinder($finder) 17 | ; 18 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. 4 | 5 | ## 0.11.2 6 | 7 | ## Added 8 | 9 | - Support for Symfony >=6.0 10 | 11 | ## 0.11.1 12 | 13 | ### Added 14 | 15 | - Support for PHP 8 16 | 17 | ## 0.11.0 18 | 19 | ### Fixed 20 | 21 | - When using `index_parameter: 'id'` we set `srcLang=x-id` in the XML sent to Loco. 22 | This will force Loco to select locale better. This will avoid problems with mixed 23 | locales like `nl-NL` and `nl`. 24 | 25 | ## 0.10.0 26 | 27 | ### Added 28 | 29 | - Support for php-translation/symfony-storage 2.1 30 | - Support for php-translation/common 3.0 31 | 32 | ## 0.9.0 33 | 34 | - Remove support of PHP < 7.2 35 | - Remove support of symfony components < 3.4 36 | - Add support for symfony ^5.0 37 | 38 | ## 0.8.0 39 | 40 | ### Added 41 | 42 | - Support for stable versions of php-translation/common and php-translation/storage 43 | 44 | ## 0.7.0 45 | 46 | ### Added 47 | 48 | - Better support for managing multiple domains in one Loco project 49 | - Support for latest php-translation/common and storage 50 | 51 | ## 0.6.2 52 | 53 | ### Fixed 54 | 55 | - Export will filter on domain. 56 | 57 | ## 0.6.1 58 | 59 | ### Fixed 60 | 61 | - Syntax error 62 | 63 | ## 0.6.0 64 | 65 | ### Added 66 | 67 | - Make sure we can configure what index key we should use with Loco. This will fix duplicate message issue. 68 | 69 | ## 0.5.0 70 | 71 | ### Added 72 | 73 | - Support for Symfony 4 74 | 75 | ## 0.4.0 76 | 77 | ### Changed 78 | 79 | - Skip creation of translations that are the same as their key 80 | - Bumped version of php-translation/symfony-storage 81 | 82 | ## 0.3.1 83 | 84 | ### Fixed 85 | 86 | - Fixed bug when Translation not found. `Loco::get` should not throw exception. 87 | 88 | ## 0.3.0 89 | 90 | ### Changed 91 | 92 | - Only export translated strings 93 | 94 | ## 0.2.1 95 | 96 | ### Added 97 | 98 | - Add the translation parameters as "Notes" in Loco. 99 | 100 | ## 0.2.0 101 | 102 | ### Added 103 | 104 | - Added support for `TransferableStorage` 105 | 106 | ### Changed 107 | 108 | - `Loco::getApiKey()` is now private 109 | 110 | ## 0.1.0 111 | 112 | Init release 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) PHP Translation team 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: ${TARGETS} 2 | 3 | DIR := ${CURDIR} 4 | QA_IMAGE := jakzal/phpqa:php8.1-alpine 5 | 6 | cs-fix: 7 | @docker run --rm -v $(DIR):/project -w /project $(QA_IMAGE) php-cs-fixer fix -vvv 8 | 9 | cs-diff: 10 | @docker run --rm -v $(DIR):/project -w /project $(QA_IMAGE) php-cs-fixer fix --dry-run -vvv 11 | 12 | phpstan: 13 | @docker run --rm -v $(DIR):/project -w /project $(QA_IMAGE) phpstan analyze 14 | 15 | phpunit: 16 | @vendor/bin/phpunit 17 | 18 | static: cs-diff phpstan 19 | 20 | test: static phpunit 21 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Adapter for Loco 2 | 3 | [![Latest Version](https://img.shields.io/github/release/php-translation/loco-adapter.svg?style=flat-square)](https://github.com/php-translation/loco-adapter/releases) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/php-translation/loco-adapter.svg?style=flat-square)](https://packagist.org/packages/php-translation/loco-adapter) 5 | 6 | This is an PHP-translation adapter for Loco ([Localise.biz](https://localise.biz/)). 7 | 8 | ### Install 9 | 10 | ```bash 11 | composer require php-translation/loco-adapter 12 | ``` 13 | 14 | ##### Symfony bundle 15 | 16 | If you want to use the Symfony bundle you may activate it in kernel: 17 | ```php 18 | // app/AppKernel.php 19 | 20 | public function registerBundles() 21 | { 22 | $bundles = array( 23 | // ... 24 | new Translation\PlatformAdapter\Loco\Bridge\Symfony\TranslationAdapterLocoBundle(), 25 | ); 26 | } 27 | ``` 28 | 29 | If you have one Loco project per domain you may configure the bundle like this: 30 | ```yaml 31 | # /app/config/config.yml 32 | translation_adapter_loco: 33 | index_parameter: 'id' # 'text' or 'name'. Leave blank for "auto" See https://localise.biz/api/docs/export/exportlocale 34 | projects: 35 | messages: 36 | api_key: 'foobar' 37 | navigation: 38 | api_key: 'bazbar' 39 | status: '!untranslated,!rejected' # if you want filter on loco translations statuses. By default only 'translated' translations are pulled. 40 | ``` 41 | 42 | If you just doing one project and have tags for all your translation domains you may use this configuration: 43 | ```yaml 44 | # /app/config/config.yml 45 | translation_adapter_loco: 46 | index_parameter: 'id' # 'text' or 'name'. Leave blank for "auto" See https://localise.biz/api/docs/export/exportlocale 47 | projects: 48 | acme: 49 | api_key: 'foobar' 50 | domains: ['messages', 'navigation'] 51 | ``` 52 | 53 | This will produce a service named `php_translation.adapter.loco` that could be used in the configuration for 54 | the [Translation Bundle](https://github.com/php-translation/symfony-bundle). 55 | 56 | If you need to override the [HTTPlug client](http://docs.php-http.org/en/latest/integrations/symfony-bundle.html#configure-clients): 57 | ```yaml 58 | # /app/config/config.yml 59 | translation_adapter_loco: 60 | httplug_client: httplug.client.loco 61 | # You can even customize the message and uri factory 62 | # httplug_message_factory: null 63 | # httplug_uri_factory: null 64 | 65 | httplug: 66 | clients: 67 | loco: 68 | factory: 'httplug.factory.guzzle6' 69 | plugins: 70 | - httplug.plugin.content_length 71 | - httplug.plugin.logger 72 | config: 73 | timeout: 2 74 | ``` 75 | By default it will use the discovery feature of HTTPlug. 76 | 77 | ### Documentation 78 | 79 | Read our documentation at [http://php-translation.readthedocs.io](http://php-translation.readthedocs.io/en/latest/). 80 | 81 | ### Contribute 82 | 83 | Do you want to make a change? Pull requests are welcome. 84 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-translation/loco-adapter", 3 | "description": "Adapter for loco.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Tobias Nyholm", 8 | "email": "tobias.nyholm@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": "^7.2 || ^8.0", 13 | "php-translation/symfony-storage": "^2.1", 14 | "symfony/translation": "^4.4.20 || ^5.2.5 || ^6.0", 15 | "symfony/yaml": "^4.4.20 || ^5.2.5 || ^6.0", 16 | "friendsofapi/localise.biz": "^1.0" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "^8.5", 20 | "symfony/framework-bundle": "^4.4.20 || ^5.2.5 || ^6.0", 21 | "nyholm/psr7": "^1.2", 22 | "nyholm/symfony-bundle-test": "^1.6.1, <1.8", 23 | "php-http/curl-client": "^2.0", 24 | "php-http/httplug-bundle": "^1.16", 25 | "php-http/message": "^1.11" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Translation\\PlatformAdapter\\Loco\\": "src/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Translation\\PlatformAdapter\\Loco\\Tests\\": "tests/" 35 | } 36 | }, 37 | "scripts": { 38 | "test": "vendor/bin/phpunit", 39 | "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml" 40 | }, 41 | "extra": { 42 | "branch-alias": { 43 | "dev-master": "0.9-dev" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Call to an undefined method FAPI\\\\Localise\\\\Model\\\\Translation\\\\Translation\\|Psr\\\\Http\\\\Message\\\\ResponseInterface\\:\\:getTranslation\\(\\)\\.$#" 5 | count: 1 6 | path: src/Loco.php 7 | 8 | - 9 | message: "#^Parameter \\#1 \\$projectKey of method FAPI\\\\Localise\\\\Api\\\\Translation\\:\\:get\\(\\) expects string, string\\|null given\\.$#" 10 | count: 2 11 | path: src/Loco.php 12 | 13 | - 14 | message: "#^Parameter \\#1 \\$projectKey of method FAPI\\\\Localise\\\\Api\\\\Asset\\:\\:create\\(\\) expects string, string\\|null given\\.$#" 15 | count: 1 16 | path: src/Loco.php 17 | 18 | - 19 | message: "#^Parameter \\#1 \\$projectKey of method FAPI\\\\Localise\\\\Api\\\\Translation\\:\\:create\\(\\) expects string, string\\|null given\\.$#" 20 | count: 3 21 | path: src/Loco.php 22 | 23 | - 24 | message: "#^Parameter \\#1 \\$projectKey of method FAPI\\\\Localise\\\\Api\\\\Asset\\:\\:tag\\(\\) expects string, string\\|null given\\.$#" 25 | count: 1 26 | path: src/Loco.php 27 | 28 | - 29 | message: "#^Parameter \\#1 \\$projectKey of method FAPI\\\\Localise\\\\Api\\\\Asset\\:\\:patch\\(\\) expects string, string\\|null given\\.$#" 30 | count: 1 31 | path: src/Loco.php 32 | 33 | - 34 | message: "#^Parameter \\#1 \\$projectKey of method FAPI\\\\Localise\\\\Api\\\\Translation\\:\\:delete\\(\\) expects string, string\\|null given\\.$#" 35 | count: 1 36 | path: src/Loco.php 37 | 38 | - 39 | message: "#^Parameter \\#1 \\$projectKey of method FAPI\\\\Localise\\\\Api\\\\Export\\:\\:locale\\(\\) expects string, string\\|null given\\.$#" 40 | count: 1 41 | path: src/Loco.php 42 | 43 | - 44 | message: "#^Parameter \\#1 \\$content of static method Translation\\\\SymfonyStorage\\\\XliffConverter\\:\\:contentToCatalogue\\(\\) expects string, Psr\\\\Http\\\\Message\\\\ResponseInterface\\|string given\\.$#" 45 | count: 1 46 | path: src/Loco.php 47 | 48 | - 49 | message: "#^Parameter \\#1 \\$catalogue of static method Translation\\\\SymfonyStorage\\\\XliffConverter\\:\\:catalogueToContent\\(\\) expects Symfony\\\\Component\\\\Translation\\\\MessageCatalogue, Symfony\\\\Component\\\\Translation\\\\MessageCatalogueInterface given\\.$#" 50 | count: 1 51 | path: src/Loco.php 52 | 53 | - 54 | message: "#^Parameter \\#1 \\$projectKey of method FAPI\\\\Localise\\\\Api\\\\Import\\:\\:import\\(\\) expects string, string\\|null given\\.$#" 55 | count: 1 56 | path: src/Loco.php 57 | 58 | - 59 | message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\TreeBuilder\\:\\:root\\(\\)\\.$#" 60 | count: 2 61 | path: src/Bridge/Symfony/DependencyInjection/Configuration.php 62 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | level: max 6 | inferPrivatePropertyTypeFromConstructor: true 7 | checkMissingIterableValueType: false 8 | paths: 9 | - src 10 | excludePaths: 11 | - vendor 12 | - tests 13 | -------------------------------------------------------------------------------- /src/Bridge/Symfony/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Translation\PlatformAdapter\Loco\Bridge\Symfony\DependencyInjection; 13 | 14 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 15 | use Symfony\Component\Config\Definition\ConfigurationInterface; 16 | 17 | /** 18 | * @author Tobias Nyholm 19 | */ 20 | class Configuration implements ConfigurationInterface 21 | { 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function getConfigTreeBuilder() 26 | { 27 | $treeBuilder = new TreeBuilder('translation_adapter_loco'); 28 | // Keep compatibility with symfony/config < 4.2 29 | if (!method_exists($treeBuilder, 'getRootNode')) { 30 | $root = $treeBuilder->root('translation_adapter_loco'); 31 | } else { 32 | $root = $treeBuilder->getRootNode(); 33 | } 34 | 35 | $root 36 | ->children() 37 | ->scalarNode('httplug_client')->defaultNull()->end() 38 | ->scalarNode('httplug_message_factory')->defaultNull()->end() 39 | ->scalarNode('httplug_uri_factory')->defaultNull()->end() 40 | ->scalarNode('index_parameter') 41 | ->info('Index parameter sent to loco api to all your domains. Specify whether file indexes translations by asset ID or source texts') 42 | ->example('id') 43 | ->defaultNull() 44 | ->end() 45 | ->append($this->getProjectNode()) 46 | ->end(); 47 | 48 | return $treeBuilder; 49 | } 50 | 51 | /** 52 | * @return \Symfony\Component\Config\Definition\Builder\NodeDefinition 53 | */ 54 | private function getProjectNode() 55 | { 56 | $treeBuilder = new TreeBuilder('projects'); 57 | // Keep compatibility with symfony/config < 4.2 58 | if (!method_exists($treeBuilder, 'getRootNode')) { 59 | $node = $treeBuilder->root('projects'); 60 | } else { 61 | $node = $treeBuilder->getRootNode(); 62 | } 63 | $node 64 | ->useAttributeAsKey('name') 65 | ->prototype('array') 66 | ->children() 67 | ->scalarNode('api_key')->isRequired()->end() 68 | ->scalarNode('status')->defaultValue('translated')->end() 69 | ->scalarNode('index_parameter') 70 | ->info('Index parameter sent to loco api for this particular domain (overrides global one). Specify whether file indexes translations by asset ID or source texts') 71 | ->example('id') 72 | ->defaultNull() 73 | ->end() 74 | ->arrayNode('domains') 75 | ->requiresAtLeastOneElement() 76 | ->prototype('scalar')->end() 77 | ->end() 78 | ->end() 79 | ->end(); 80 | 81 | return $node; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Bridge/Symfony/DependencyInjection/TranslationAdapterLocoExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Translation\PlatformAdapter\Loco\Bridge\Symfony\DependencyInjection; 13 | 14 | use FAPI\Localise\HttpClientConfigurator; 15 | use FAPI\Localise\LocoClient; 16 | use FAPI\Localise\RequestBuilder; 17 | use Symfony\Component\DependencyInjection\ContainerBuilder; 18 | use Symfony\Component\DependencyInjection\Definition; 19 | use Symfony\Component\DependencyInjection\Reference; 20 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 21 | use Translation\PlatformAdapter\Loco\Loco; 22 | use Translation\PlatformAdapter\Loco\Model\LocoProject; 23 | 24 | /** 25 | * @author Tobias Nyholm 26 | */ 27 | class TranslationAdapterLocoExtension extends Extension 28 | { 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function load(array $configs, ContainerBuilder $container): void 33 | { 34 | $configuration = new Configuration(); 35 | $config = $this->processConfiguration($configuration, $configs); 36 | 37 | $projects = []; 38 | $globalIndexParameter = $config['index_parameter']; 39 | foreach ($config['projects'] as $name => $data) { 40 | if (empty($data['index_parameter'])) { 41 | $data['index_parameter'] = $globalIndexParameter; 42 | } 43 | 44 | $projectDefinition = new Definition(LocoProject::class); 45 | $projectDefinition 46 | ->addArgument($name) 47 | ->addArgument($data); 48 | $projects[] = $projectDefinition; 49 | } 50 | 51 | $requestBuilder = (new Definition(RequestBuilder::class)) 52 | ->addArgument(empty($config['httplug_message_factory']) ? null : new Reference($config['httplug_message_factory'])); 53 | 54 | $clientConfigurator = (new Definition(HttpClientConfigurator::class)) 55 | ->addArgument(empty($config['httplug_client']) ? null : new Reference($config['httplug_client'])) 56 | ->addArgument(empty($config['httplug_uri_factory']) ? null : new Reference($config['httplug_uri_factory'])); 57 | 58 | $apiDef = $container->register('php_translation.adapter.loco.raw'); 59 | $apiDef->setClass(LocoClient::class) 60 | ->setFactory([LocoClient::class, 'configure']) 61 | ->setPublic(true) 62 | ->addArgument($clientConfigurator) 63 | ->addArgument(null) 64 | ->addArgument($requestBuilder); 65 | 66 | $adapterDef = $container->register('php_translation.adapter.loco'); 67 | $adapterDef 68 | ->setClass(Loco::class) 69 | ->setPublic(true) 70 | ->addArgument($apiDef) 71 | ->addArgument($projects); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Bridge/Symfony/TranslationAdapterLocoBundle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Translation\PlatformAdapter\Loco\Bridge\Symfony; 13 | 14 | use Symfony\Component\HttpKernel\Bundle\Bundle; 15 | 16 | /** 17 | * @author Tobias Nyholm 18 | */ 19 | class TranslationAdapterLocoBundle extends Bundle 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/Loco.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Translation\PlatformAdapter\Loco; 13 | 14 | use FAPI\Localise\Exception\Domain\AssetConflictException; 15 | use FAPI\Localise\Exception\Domain\NotFoundException; 16 | use FAPI\Localise\LocoClient; 17 | use Symfony\Component\Translation\MessageCatalogueInterface; 18 | use Symfony\Component\Yaml\Yaml; 19 | use Translation\Common\Exception\StorageException; 20 | use Translation\Common\Model\Message; 21 | use Translation\Common\Model\MessageInterface; 22 | use Translation\Common\Storage; 23 | use Translation\Common\TransferableStorage; 24 | use Translation\PlatformAdapter\Loco\Model\LocoProject; 25 | use Translation\SymfonyStorage\XliffConverter; 26 | 27 | /** 28 | * Localize.biz. 29 | * 30 | * @author Tobias Nyholm 31 | */ 32 | class Loco implements Storage, TransferableStorage 33 | { 34 | /** 35 | * @var LocoClient 36 | */ 37 | private $client; 38 | 39 | /** 40 | * @var LocoProject[] 41 | */ 42 | private $projects = []; 43 | 44 | /** 45 | * @param LocoProject[] $projects 46 | */ 47 | public function __construct(LocoClient $client, array $projects) 48 | { 49 | $this->client = $client; 50 | $this->projects = $projects; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function get(string $locale, string $domain, string $key): ?MessageInterface 57 | { 58 | $project = $this->getProject($domain); 59 | 60 | try { 61 | $translation = $this->client->translations()->get($project->getApiKey(), $key, $locale)->getTranslation(); 62 | } catch (\FAPI\Localise\Exception $e) { 63 | return null; 64 | } 65 | $meta = []; 66 | 67 | return new Message($key, $domain, $locale, $translation, $meta); 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function create(MessageInterface $message): void 74 | { 75 | $project = $this->getProject($message->getDomain()); 76 | $isNewAsset = true; 77 | 78 | try { 79 | // Create asset first 80 | $this->client->asset()->create($project->getApiKey(), $message->getKey()); 81 | } catch (AssetConflictException $e) { 82 | // This is okey 83 | $isNewAsset = false; 84 | } 85 | 86 | $translation = $message->getTranslation(); 87 | 88 | // translation is the same as the key, so we will set it to empty string 89 | // as it was not translated and stats on loco will be unaffected 90 | if ($message->getKey() === $message->getTranslation()) { 91 | $translation = ''; 92 | } 93 | 94 | if ($isNewAsset) { 95 | $this->client->translations()->create( 96 | $project->getApiKey(), 97 | $message->getKey(), 98 | $message->getLocale(), 99 | $translation 100 | ); 101 | } else { 102 | try { 103 | $this->client->translations()->get( 104 | $project->getApiKey(), 105 | $message->getKey(), 106 | $message->getLocale() 107 | ); 108 | } catch (NotFoundException $e) { 109 | // Create only if not found. 110 | $this->client->translations()->create( 111 | $project->getApiKey(), 112 | $message->getKey(), 113 | $message->getLocale(), 114 | $translation 115 | ); 116 | } 117 | } 118 | 119 | $this->client->asset()->tag($project->getApiKey(), $message->getKey(), $message->getDomain()); 120 | 121 | if (!empty($message->getMeta('parameters'))) { 122 | // Pretty print the Meta field via YAML export 123 | $dump = Yaml::dump(['parameters' => $message->getMeta('parameters')], 4, 5); 124 | $dump = str_replace(" -\n", '', $dump); 125 | $dump = str_replace(' ', "\xC2\xA0", $dump); // no break space 126 | 127 | $this->client->asset()->patch( 128 | $project->getApiKey(), 129 | $message->getKey(), 130 | null, 131 | null, 132 | null, 133 | $dump 134 | ); 135 | } 136 | } 137 | 138 | /** 139 | * {@inheritdoc} 140 | */ 141 | public function update(MessageInterface $message): void 142 | { 143 | $project = $this->getProject($message->getDomain()); 144 | 145 | try { 146 | $this->client->translations()->create($project->getApiKey(), $message->getKey(), $message->getLocale(), $message->getTranslation()); 147 | } catch (NotFoundException $e) { 148 | $this->create($message); 149 | } 150 | } 151 | 152 | /** 153 | * {@inheritdoc} 154 | */ 155 | public function delete(string $locale, string $domain, string $key): void 156 | { 157 | $project = $this->getProject($domain); 158 | 159 | $this->client->translations()->delete($project->getApiKey(), $key, $locale); 160 | } 161 | 162 | /** 163 | * {@inheritdoc} 164 | */ 165 | public function export(MessageCatalogueInterface $catalogue, array $options = []): void 166 | { 167 | $locale = $catalogue->getLocale(); 168 | foreach ($this->projects as $project) { 169 | foreach ($project->getDomains() as $domain) { 170 | try { 171 | $params = [ 172 | 'format' => 'symfony', 173 | 'index' => $project->getIndexParameter(), 174 | ]; 175 | 176 | if ($project->getStatus()) { 177 | $params['status'] = $project->getStatus(); 178 | } 179 | 180 | if ($project->isMultiDomain()) { 181 | $params['filter'] = $domain; 182 | } 183 | 184 | $data = $this->client->export()->locale($project->getApiKey(), $locale, 'xliff', $params); 185 | $catalogue->addCatalogue(XliffConverter::contentToCatalogue($data, $locale, $domain)); 186 | } catch (NotFoundException $e) { 187 | } 188 | } 189 | } 190 | } 191 | 192 | /** 193 | * {@inheritdoc} 194 | */ 195 | public function import(MessageCatalogueInterface $catalogue, array $options = []): void 196 | { 197 | $locale = $catalogue->getLocale(); 198 | foreach ($this->projects as $project) { 199 | foreach ($project->getDomains() as $domain) { 200 | if ('id' === $project->getIndexParameter()) { 201 | $options['default_locale'] = 'x-id'; 202 | } 203 | 204 | $data = XliffConverter::catalogueToContent($catalogue, $domain, $options); 205 | $params = [ 206 | 'locale' => $locale, 207 | 'async' => 1, 208 | 'index' => $project->getIndexParameter(), 209 | ]; 210 | 211 | if ($project->isMultiDomain()) { 212 | $params['tag-all'] = $domain; 213 | } 214 | 215 | $this->client->import()->import($project->getApiKey(), 'xliff', $data, $params); 216 | } 217 | } 218 | } 219 | 220 | private function getProject(string $domain): LocoProject 221 | { 222 | foreach ($this->projects as $project) { 223 | if ($project->hasDomain($domain)) { 224 | return $project; 225 | } 226 | } 227 | 228 | throw new StorageException(sprintf('Project for "%s" domain was not found.', $domain)); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/Model/LocoProject.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Translation\PlatformAdapter\Loco\Model; 13 | 14 | /** 15 | * Represents a project from loco. 16 | */ 17 | final class LocoProject 18 | { 19 | /** 20 | * @var string 21 | */ 22 | private $name; 23 | 24 | /** 25 | * @var string 26 | */ 27 | private $apiKey; 28 | 29 | /** 30 | * @var string 31 | */ 32 | private $status; 33 | 34 | /** 35 | * @var string 36 | */ 37 | private $indexParameter; 38 | 39 | /** 40 | * @var array 41 | */ 42 | private $domains; 43 | 44 | public function __construct(string $name, array $config) 45 | { 46 | $this->name = $name; 47 | $this->apiKey = $config['api_key'] ?? null; 48 | $this->status = $config['status'] ?? null; 49 | $this->indexParameter = $config['index_parameter'] ?? null; 50 | $this->domains = empty($config['domains']) ? [$name] : $config['domains']; 51 | } 52 | 53 | public function getName(): string 54 | { 55 | return $this->name; 56 | } 57 | 58 | public function getApiKey(): ?string 59 | { 60 | return $this->apiKey; 61 | } 62 | 63 | public function getStatus(): ?string 64 | { 65 | return $this->status; 66 | } 67 | 68 | public function getIndexParameter(): ?string 69 | { 70 | return $this->indexParameter; 71 | } 72 | 73 | public function getDomains(): array 74 | { 75 | return $this->domains; 76 | } 77 | 78 | public function hasDomain(string $domain): bool 79 | { 80 | return \in_array($domain, $this->domains); 81 | } 82 | 83 | /** 84 | * Returning true means that domains are expected to be managed with tags. 85 | */ 86 | public function isMultiDomain(): bool 87 | { 88 | return \count($this->domains) > 1; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-translation/loco-adapter/ef2b0f36ca06e5c819bab655bb8546110d3e8afb/tests/.gitkeep -------------------------------------------------------------------------------- /tests/Functional/BundleInitializationTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Translation\PlatformAdapter\Loco\Tests\Functional; 13 | 14 | use Http\HttplugBundle\HttplugBundle; 15 | use Nyholm\BundleTest\BaseBundleTestCase; 16 | use Translation\PlatformAdapter\Loco\Bridge\Symfony\TranslationAdapterLocoBundle; 17 | use Translation\PlatformAdapter\Loco\Loco; 18 | 19 | class BundleInitializationTest extends BaseBundleTestCase 20 | { 21 | protected function getBundleClass() 22 | { 23 | return TranslationAdapterLocoBundle::class; 24 | } 25 | 26 | public function testRegisterBundle() 27 | { 28 | $kernel = $this->createKernel(); 29 | $kernel->addBundle(HttplugBundle::class); 30 | 31 | $kernel->boot(); 32 | $container = $kernel->getContainer(); 33 | 34 | $this->assertTrue($container->has('php_translation.adapter.loco')); 35 | $service = $container->get('php_translation.adapter.loco'); 36 | $this->assertInstanceOf(Loco::class, $service); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Unit/LocoTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Translation\PlatformAdapter\Loco\Tests\Unit; 15 | 16 | use FAPI\Localise\Hydrator\Hydrator; 17 | use FAPI\Localise\LocoClient; 18 | use Http\Client\HttpClient; 19 | use PHPUnit\Framework\MockObject\MockObject; 20 | use PHPUnit\Framework\TestCase; 21 | use Psr\Http\Message\RequestInterface; 22 | use Psr\Http\Message\ResponseInterface; 23 | use Psr\Http\Message\StreamInterface; 24 | use Symfony\Component\Translation\MessageCatalogue; 25 | use Translation\PlatformAdapter\Loco\Loco; 26 | use Translation\PlatformAdapter\Loco\Model\LocoProject; 27 | 28 | class LocoTest extends TestCase 29 | { 30 | /** 31 | * @var LocoClient 32 | */ 33 | private $client; 34 | 35 | /** 36 | * @var HttpClient|MockObject 37 | */ 38 | private $httpClient; 39 | 40 | /** 41 | * @var Hydrator|MockObject 42 | */ 43 | private $hydrator; 44 | 45 | protected function setUp(): void 46 | { 47 | $this->httpClient = $this->createMock(HttpClient::class); 48 | $this->hydrator = $this->createMock(Hydrator::class); 49 | $this->client = new LocoClient($this->httpClient, $this->hydrator); 50 | } 51 | 52 | public function testOverridesTheDefaultLocaleWhenUsingTranslationKeys(): void 53 | { 54 | $locoProject = new LocoProject('main', ['api_key' => 'FooBar', 'index_parameter' => 'id']); 55 | $loco = new Loco($this->client, [$locoProject]); 56 | 57 | $catalogue = new MessageCatalogue('nl', []); 58 | 59 | $response = $this->createMock(ResponseInterface::class); 60 | $this->httpClient 61 | ->method('sendRequest') 62 | ->with( 63 | $this->callback( 64 | // Capture the request body so we can make assertions on it later on 65 | function (RequestInterface $argument) use (&$body): bool { 66 | $body = $argument->getBody()->__toString(); 67 | 68 | return true; 69 | } 70 | ) 71 | ) 72 | ->willReturn($response); 73 | 74 | $stream = $this->createMock(StreamInterface::class); 75 | $response->method('getBody')->willReturn($stream); 76 | $stream->method('__toString')->willReturn('{}'); 77 | $response->method('getStatusCode')->willReturn(201); 78 | 79 | $loco->import($catalogue); 80 | 81 | $this->assertStringContainsString( 82 | '', 83 | $body 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/Unit/Model/LocoProjectTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Translation\PlatformAdapter\Loco\Tests\Unit\Model; 13 | 14 | use PHPUnit\Framework\TestCase; 15 | use Translation\PlatformAdapter\Loco\Model\LocoProject; 16 | 17 | class LocoProjectTest extends TestCase 18 | { 19 | /** 20 | * @var LocoProject 21 | */ 22 | private $locoProject; 23 | 24 | protected function setUp(): void 25 | { 26 | $this->locoProject = new LocoProject('domain', ['api_key' => 'test', 'status' => '!untranslated,!rejected', 'index_parameter' => 'text']); 27 | } 28 | 29 | public function testWithEmptyConfig() 30 | { 31 | $locoProject = new LocoProject('domain', []); 32 | $this->assertNull($locoProject->getIndexParameter()); 33 | $this->assertNull($locoProject->getApiKey()); 34 | } 35 | 36 | public function testGetName() 37 | { 38 | $this->assertEquals('domain', $this->locoProject->getName()); 39 | } 40 | 41 | public function testGetApiKey() 42 | { 43 | $this->assertEquals('test', $this->locoProject->getApiKey()); 44 | } 45 | 46 | public function testGetStatus() 47 | { 48 | $this->assertEquals('!untranslated,!rejected', $this->locoProject->getStatus()); 49 | } 50 | 51 | public function testGetIndexParameter() 52 | { 53 | $this->assertEquals('text', $this->locoProject->getIndexParameter()); 54 | } 55 | } 56 | --------------------------------------------------------------------------------