feature #40926 [Translation] Added PoEditor Provider (welcoMattic)
This PR was merged into the 5.3-dev branch.
Discussion
----------
[Translation] Added PoEditor Provider
| Q | A
| ------------- | ---
| Branch? | 5.x
| Bug fix? | no
| New feature? | no
| Deprecations? | no
| Tickets |
| License | MIT
| Doc PR | https://github.com/symfony/symfony-docs/pull/15310
To follow up on https://github.com/symfony/symfony/pull/38475, this PR adds [PoEditor](https://poeditor.com/) Provider.
The todo list to make it ready is:
- [x] Apply recent changes that have been made on `ProviderInterface` and `TranslatorBagInterface` (we removed the `all()` and `getDomains()` method from TranslatorBagInterface)
- [x] Add PoEditorProvider to `src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_providers.php` file
- [x] Add PoEditor case to `Symfony\Component\Translation\Exception\UnsupportedSchemeException`
- [x] Write integration tests by mocking HTTP Responses
The major part of the remaining work concerns tests, I will make it done before the beginning of May.
Commits
-------
240ac22f70
Added PoEditor Provider
This commit is contained in:
commit
af19b6bec2
@ -171,6 +171,7 @@ use Symfony\Component\String\LazyString;
|
||||
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||
use Symfony\Component\Translation\Bridge\Crowdin\CrowdinProviderFactory;
|
||||
use Symfony\Component\Translation\Bridge\Loco\LocoProviderFactory;
|
||||
use Symfony\Component\Translation\Bridge\PoEditor\PoEditorProviderFactory;
|
||||
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
|
||||
use Symfony\Component\Translation\PseudoLocalizationTranslator;
|
||||
use Symfony\Component\Translation\Translator;
|
||||
@ -1344,14 +1345,17 @@ class FrameworkExtension extends Extension
|
||||
$classToServices = [
|
||||
CrowdinProviderFactory::class => 'translation.provider_factory.crowdin',
|
||||
LocoProviderFactory::class => 'translation.provider_factory.loco',
|
||||
PoEditorProviderFactory::class => 'translation.provider_factory.poeditor',
|
||||
];
|
||||
|
||||
$parentPackages = ['symfony/framework-bundle', 'symfony/translation', 'symfony/http-client'];
|
||||
|
||||
foreach ($classToServices as $class => $service) {
|
||||
$package = sprintf('symfony/%s-translation-provider', substr($service, \strlen('translation.provider_factory.')));
|
||||
switch ($package = substr($service, \strlen('translation.provider_factory.'))) {
|
||||
case 'poeditor': $package = 'po-editor'; break;
|
||||
}
|
||||
|
||||
if (!$container->hasDefinition('http_client') || !ContainerBuilder::willBeAvailable($package, $class, $parentPackages)) {
|
||||
if (!$container->hasDefinition('http_client') || !ContainerBuilder::willBeAvailable(sprintf('symfony/%s-translation-provider', $package), $class, $parentPackages)) {
|
||||
$container->removeDefinition($service);
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
||||
|
||||
use Symfony\Component\Translation\Bridge\Crowdin\CrowdinProviderFactory;
|
||||
use Symfony\Component\Translation\Bridge\Loco\LocoProviderFactory;
|
||||
use Symfony\Component\Translation\Bridge\PoEditor\PoEditorProviderFactory;
|
||||
use Symfony\Component\Translation\Provider\NullProviderFactory;
|
||||
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
|
||||
use Symfony\Component\Translation\Provider\TranslationProviderCollectionFactory;
|
||||
@ -52,5 +53,14 @@ return static function (ContainerConfigurator $container) {
|
||||
service('translation.loader.xliff'),
|
||||
])
|
||||
->tag('translation.provider_factory')
|
||||
|
||||
->set('translation.provider_factory.poeditor', PoEditorProviderFactory::class)
|
||||
->args([
|
||||
service('http_client'),
|
||||
service('logger'),
|
||||
param('kernel.default_locale'),
|
||||
service('translation.loader.xliff'),
|
||||
])
|
||||
->tag('translation.provider_factory')
|
||||
;
|
||||
};
|
||||
|
@ -23,7 +23,7 @@ where:
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
n the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
|
@ -19,7 +19,7 @@ where:
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
|
4
src/Symfony/Component/Translation/Bridge/PoEditor/.gitattributes
vendored
Normal file
4
src/Symfony/Component/Translation/Bridge/PoEditor/.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/Tests export-ignore
|
||||
/phpunit.xml.dist export-ignore
|
||||
/.gitattributes export-ignore
|
||||
/.gitignore export-ignore
|
3
src/Symfony/Component/Translation/Bridge/PoEditor/.gitignore
vendored
Normal file
3
src/Symfony/Component/Translation/Bridge/PoEditor/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
vendor/
|
||||
composer.lock
|
||||
phpunit.xml
|
@ -0,0 +1,7 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.3
|
||||
---
|
||||
|
||||
* Create the bridge
|
19
src/Symfony/Component/Translation/Bridge/PoEditor/LICENSE
Normal file
19
src/Symfony/Component/Translation/Bridge/PoEditor/LICENSE
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2021 Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Translation\Bridge\PoEditor;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Translation\Exception\ProviderException;
|
||||
use Symfony\Component\Translation\Loader\LoaderInterface;
|
||||
use Symfony\Component\Translation\Provider\ProviderInterface;
|
||||
use Symfony\Component\Translation\TranslatorBag;
|
||||
use Symfony\Component\Translation\TranslatorBagInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||
*
|
||||
* In PoEditor:
|
||||
* * Terms refer to Symfony's translation keys;
|
||||
* * Translations refer to Symfony's translated messages;
|
||||
* * Context fields refer to Symfony's translation domains
|
||||
*
|
||||
* PoEditor's API always returns 200 status code, even in case of failure.
|
||||
*
|
||||
* @experimental in 5.3
|
||||
*/
|
||||
final class PoEditorProvider implements ProviderInterface
|
||||
{
|
||||
private $apiKey;
|
||||
private $projectId;
|
||||
private $client;
|
||||
private $loader;
|
||||
private $logger;
|
||||
private $defaultLocale;
|
||||
private $endpoint;
|
||||
|
||||
public function __construct(string $apiKey, string $projectId, HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, string $defaultLocale, string $endpoint)
|
||||
{
|
||||
$this->apiKey = $apiKey;
|
||||
$this->projectId = $projectId;
|
||||
$this->client = $client;
|
||||
$this->loader = $loader;
|
||||
$this->logger = $logger;
|
||||
$this->defaultLocale = $defaultLocale;
|
||||
$this->endpoint = $endpoint;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf('poeditor://%s', $this->endpoint);
|
||||
}
|
||||
|
||||
public function write(TranslatorBagInterface $translatorBag): void
|
||||
{
|
||||
$defaultCatalogue = $translatorBag->getCatalogue($this->defaultLocale);
|
||||
|
||||
if (!$defaultCatalogue) {
|
||||
$defaultCatalogue = $translatorBag->getCatalogues()[0];
|
||||
}
|
||||
|
||||
$terms = $translationsToAdd = [];
|
||||
foreach ($defaultCatalogue->all() as $domain => $messages) {
|
||||
foreach ($messages as $id => $message) {
|
||||
$terms[] = [
|
||||
'term' => $id,
|
||||
'reference' => $id,
|
||||
// tags field is mandatory to export all translations in read method.
|
||||
'tags' => [$domain],
|
||||
'context' => $domain,
|
||||
];
|
||||
}
|
||||
}
|
||||
$this->addTerms($terms);
|
||||
|
||||
foreach ($translatorBag->getCatalogues() as $catalogue) {
|
||||
$locale = $catalogue->getLocale();
|
||||
foreach ($catalogue->all() as $domain => $messages) {
|
||||
foreach ($messages as $id => $message) {
|
||||
$translationsToAdd[$locale][] = [
|
||||
'term' => $id,
|
||||
'context' => $domain,
|
||||
'translation' => [
|
||||
'content' => $message,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->addTranslations($translationsToAdd);
|
||||
}
|
||||
|
||||
public function read(array $domains, array $locales): TranslatorBag
|
||||
{
|
||||
$translatorBag = new TranslatorBag();
|
||||
$exportResponses = $downloadResponses = [];
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
foreach ($domains as $domain) {
|
||||
$exportResponses[] = [
|
||||
'response' => $this->client->request('POST', 'projects/export', [
|
||||
'body' => [
|
||||
'api_token' => $this->apiKey,
|
||||
'id' => $this->projectId,
|
||||
'language' => $locale,
|
||||
'type' => 'xlf',
|
||||
'filters' => json_encode(['translated']),
|
||||
'tags' => json_encode([$domain]),
|
||||
],
|
||||
]),
|
||||
'locale' => $locale,
|
||||
'domain' => $domain,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($exportResponses as $exportResponse) {
|
||||
$response = $exportResponse['response'];
|
||||
$responseContent = $response->toArray(false);
|
||||
|
||||
if (200 !== $response->getStatusCode() || '200' !== (string) $responseContent['response']['code']) {
|
||||
$this->logger->error('Unable to read the PoEditor response: '.$response->getContent(false));
|
||||
continue;
|
||||
}
|
||||
|
||||
$fileUrl = $responseContent['result']['url'];
|
||||
$downloadResponses[] = [
|
||||
'response' => $this->client->request('GET', $fileUrl),
|
||||
'locale' => $exportResponse['locale'],
|
||||
'domain' => $exportResponse['domain'],
|
||||
'fileUrl' => $fileUrl,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($downloadResponses as $downloadResponse) {
|
||||
$response = $downloadResponse['response'];
|
||||
$locale = $downloadResponse['locale'];
|
||||
$domain = $downloadResponse['domain'];
|
||||
$fileUrl = $downloadResponse['fileUrl'];
|
||||
$responseContent = $response->getContent(false);
|
||||
|
||||
if (200 !== $response->getStatusCode()) {
|
||||
$this->logger->error('Unable to download the PoEditor exported file: '.$responseContent);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$responseContent) {
|
||||
$this->logger->error(sprintf('The exported file "%s" from PoEditor is empty.', $fileUrl));
|
||||
continue;
|
||||
}
|
||||
|
||||
$translatorBag->addCatalogue($this->loader->load($responseContent, $locale, $domain));
|
||||
}
|
||||
|
||||
return $translatorBag;
|
||||
}
|
||||
|
||||
public function delete(TranslatorBagInterface $translatorBag): void
|
||||
{
|
||||
$deletedIds = $termsToDelete = [];
|
||||
|
||||
foreach ($translatorBag->getCatalogues() as $catalogue) {
|
||||
foreach ($catalogue->all() as $domain => $messages) {
|
||||
foreach ($messages as $id => $message) {
|
||||
if (\array_key_exists($domain, $deletedIds) && \in_array($id, $deletedIds[$domain], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$deletedIds[$domain][] = $id;
|
||||
$termsToDelete[] = [
|
||||
'term' => $id,
|
||||
'context' => $domain,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->deleteTerms($termsToDelete);
|
||||
}
|
||||
|
||||
private function addTerms(array $terms): void
|
||||
{
|
||||
$response = $this->client->request('POST', 'terms/add', [
|
||||
'body' => [
|
||||
'api_token' => $this->apiKey,
|
||||
'id' => $this->projectId,
|
||||
'data' => json_encode($terms),
|
||||
],
|
||||
]);
|
||||
|
||||
if (200 !== $response->getStatusCode() || '200' !== (string) $response->toArray(false)['response']['code']) {
|
||||
throw new ProviderException(sprintf('Unable to add new translation keys to PoEditor: (status code: "%s") "%s".', $response->getStatusCode(), $response->getContent(false)), $response);
|
||||
}
|
||||
}
|
||||
|
||||
private function addTranslations(array $translationsPerLocale): void
|
||||
{
|
||||
$responses = [];
|
||||
|
||||
foreach ($translationsPerLocale as $locale => $translations) {
|
||||
$responses = $this->client->request('POST', 'translations/add', [
|
||||
'body' => [
|
||||
'api_token' => $this->apiKey,
|
||||
'id' => $this->projectId,
|
||||
'language' => $locale,
|
||||
'data' => json_encode($translations),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($responses as $response) {
|
||||
if (200 !== $response->getStatusCode() || '200' !== (string) $response->toArray(false)['response']['code']) {
|
||||
$this->logger->error(sprintf('Unable to add translation messages to PoEditor: "%s".', $response->getContent(false)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteTerms(array $ids): void
|
||||
{
|
||||
$response = $this->client->request('POST', 'terms/delete', [
|
||||
'body' => [
|
||||
'api_token' => $this->apiKey,
|
||||
'id' => $this->projectId,
|
||||
'data' => json_encode($ids),
|
||||
],
|
||||
]);
|
||||
|
||||
if (200 !== $response->getStatusCode() || '200' !== (string) $response->toArray(false)['response']['code']) {
|
||||
throw new ProviderException(sprintf('Unable to delete translation keys on PoEditor: "%s".', $response->getContent(false)), $response);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Translation\Bridge\PoEditor;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Translation\Exception\UnsupportedSchemeException;
|
||||
use Symfony\Component\Translation\Loader\LoaderInterface;
|
||||
use Symfony\Component\Translation\Provider\AbstractProviderFactory;
|
||||
use Symfony\Component\Translation\Provider\Dsn;
|
||||
use Symfony\Component\Translation\Provider\ProviderInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
/**
|
||||
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||
*
|
||||
* @experimental in 5.3
|
||||
*/
|
||||
final class PoEditorProviderFactory extends AbstractProviderFactory
|
||||
{
|
||||
private const HOST = 'api.poeditor.com';
|
||||
|
||||
private $client;
|
||||
private $logger;
|
||||
private $defaultLocale;
|
||||
private $loader;
|
||||
|
||||
public function __construct(HttpClientInterface $client, LoggerInterface $logger, string $defaultLocale, LoaderInterface $loader)
|
||||
{
|
||||
$this->client = $client;
|
||||
$this->logger = $logger;
|
||||
$this->defaultLocale = $defaultLocale;
|
||||
$this->loader = $loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PoEditorProvider
|
||||
*/
|
||||
public function create(Dsn $dsn): ProviderInterface
|
||||
{
|
||||
if ('poeditor' !== $dsn->getScheme()) {
|
||||
throw new UnsupportedSchemeException($dsn, 'poeditor', $this->getSupportedSchemes());
|
||||
}
|
||||
|
||||
$endpoint = sprintf('%s%s', 'default' === $dsn->getHost() ? self::HOST : $dsn->getHost(), $dsn->getPort() ? ':'.$dsn->getPort() : '');
|
||||
$client = $this->client->withOptions([
|
||||
'base_uri' => 'https://'.$endpoint.'/v2/',
|
||||
]);
|
||||
|
||||
return new PoEditorProvider($this->getPassword($dsn), $this->getUser($dsn), $client, $this->loader, $this->logger, $this->defaultLocale, $endpoint);
|
||||
}
|
||||
|
||||
protected function getSupportedSchemes(): array
|
||||
{
|
||||
return ['poeditor'];
|
||||
}
|
||||
}
|
28
src/Symfony/Component/Translation/Bridge/PoEditor/README.md
Normal file
28
src/Symfony/Component/Translation/Bridge/PoEditor/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
PoEditor Translation Provider
|
||||
=============================
|
||||
|
||||
Provides PoEditor integration for Symfony Translation.
|
||||
|
||||
DSN example
|
||||
-----------
|
||||
|
||||
```
|
||||
// .env file
|
||||
POEDITOR_DSN=poeditor://PROJECT_ID:API_KEY@default
|
||||
```
|
||||
|
||||
where:
|
||||
- `PROJECT_ID` is your PoEditor Project ID
|
||||
- `API_KEY` is your PoEditor API key
|
||||
|
||||
Go to the Project in PoEditor to find the Project ID in the url.
|
||||
|
||||
[Generate an API key on PoEditor](https://poeditor.com/account/api)
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Translation\Bridge\PoEditor\Tests;
|
||||
|
||||
use Symfony\Component\Translation\Bridge\PoEditor\PoEditorProviderFactory;
|
||||
use Symfony\Component\Translation\Provider\ProviderFactoryInterface;
|
||||
use Symfony\Component\Translation\Test\ProviderFactoryTestCase;
|
||||
|
||||
class PoEditorProviderFactoryTest extends ProviderFactoryTestCase
|
||||
{
|
||||
public function supportsProvider(): iterable
|
||||
{
|
||||
yield [true, 'poeditor://PROJECT_ID:API_KEY@default'];
|
||||
yield [false, 'somethingElse://PROJECT_ID:API_KEY@default'];
|
||||
}
|
||||
|
||||
public function unsupportedSchemeProvider(): iterable
|
||||
{
|
||||
yield ['somethingElse://PROJECT_ID:API_KEY@default'];
|
||||
}
|
||||
|
||||
public function createProvider(): iterable
|
||||
{
|
||||
yield [
|
||||
'poeditor://api.poeditor.com',
|
||||
'poeditor://PROJECT_ID:API_KEY@default',
|
||||
];
|
||||
}
|
||||
|
||||
public function incompleteDsnProvider(): iterable
|
||||
{
|
||||
yield ['poeditor://default'];
|
||||
}
|
||||
|
||||
public function createFactory(): ProviderFactoryInterface
|
||||
{
|
||||
return new PoEditorProviderFactory($this->getClient(), $this->getLogger(), $this->getDefaultLocale(), $this->getLoader());
|
||||
}
|
||||
}
|
@ -0,0 +1,514 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Translation\Bridge\PoEditor\Tests;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
use Symfony\Component\Translation\Bridge\PoEditor\PoEditorProvider;
|
||||
use Symfony\Component\Translation\Loader\ArrayLoader;
|
||||
use Symfony\Component\Translation\Loader\LoaderInterface;
|
||||
use Symfony\Component\Translation\Loader\XliffFileLoader;
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
use Symfony\Component\Translation\Provider\ProviderInterface;
|
||||
use Symfony\Component\Translation\Test\ProviderTestCase;
|
||||
use Symfony\Component\Translation\TranslatorBag;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class PoEditorProviderTest extends ProviderTestCase
|
||||
{
|
||||
public function createProvider(HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, string $defaultLocale, string $endpoint): ProviderInterface
|
||||
{
|
||||
return new PoEditorProvider('API_KEY', 'PROJECT_ID', $client, $loader, $logger, $defaultLocale, $endpoint);
|
||||
}
|
||||
|
||||
public function toStringProvider(): iterable
|
||||
{
|
||||
yield [
|
||||
$this->createProvider($this->getClient()->withOptions([
|
||||
'base_uri' => 'api.poeditor.com',
|
||||
]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.poeditor.com'),
|
||||
'poeditor://api.poeditor.com',
|
||||
];
|
||||
|
||||
yield [
|
||||
$this->createProvider($this->getClient()->withOptions([
|
||||
'base_uri' => 'https://example.com',
|
||||
]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'example.com'),
|
||||
'poeditor://example.com',
|
||||
];
|
||||
|
||||
yield [
|
||||
$this->createProvider($this->getClient()->withOptions([
|
||||
'base_uri' => 'https://example.com:99',
|
||||
]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'example.com:99'),
|
||||
'poeditor://example.com:99',
|
||||
];
|
||||
}
|
||||
|
||||
public function testCompleteWriteProcess()
|
||||
{
|
||||
$successResponse = new MockResponse(json_encode([
|
||||
'response' => [
|
||||
'status' => 'success',
|
||||
'code' => '200',
|
||||
'message' => 'OK',
|
||||
],
|
||||
]));
|
||||
|
||||
$responses = [
|
||||
'addTerms' => function (string $method, string $url, array $options = []) use ($successResponse): MockResponse {
|
||||
$this->assertSame('POST', $method);
|
||||
$this->assertSame(http_build_query([
|
||||
'api_token' => 'API_KEY',
|
||||
'id' => 'PROJECT_ID',
|
||||
'data' => json_encode([
|
||||
[
|
||||
'term' => 'a',
|
||||
'reference' => 'a',
|
||||
'tags' => ['messages'],
|
||||
'context' => 'messages',
|
||||
],
|
||||
[
|
||||
'term' => 'post.num_comments',
|
||||
'reference' => 'post.num_comments',
|
||||
'tags' => ['validators'],
|
||||
'context' => 'validators',
|
||||
],
|
||||
]),
|
||||
]), $options['body']);
|
||||
|
||||
return $successResponse;
|
||||
},
|
||||
'addTranslationsEn' => function (string $method, string $url, array $options = []) use ($successResponse): MockResponse {
|
||||
$this->assertSame('POST', $method);
|
||||
$this->assertSame(http_build_query([
|
||||
'api_token' => 'API_KEY',
|
||||
'id' => 'PROJECT_ID',
|
||||
'language' => 'en',
|
||||
'data' => json_encode([
|
||||
[
|
||||
'term' => 'a',
|
||||
'context' => 'messages',
|
||||
'translation' => ['content' => 'trans_en_a'],
|
||||
],
|
||||
[
|
||||
'term' => 'post.num_comments',
|
||||
'context' => 'validators',
|
||||
'translation' => ['content' => '{count, plural, one {# comment} other {# comments}}'],
|
||||
],
|
||||
]),
|
||||
]), $options['body']);
|
||||
|
||||
return $successResponse;
|
||||
},
|
||||
'addTranslationsFr' => function (string $method, string $url, array $options = []) use ($successResponse): MockResponse {
|
||||
$this->assertSame('POST', $method);
|
||||
$this->assertSame(http_build_query([
|
||||
'api_token' => 'API_KEY',
|
||||
'id' => 'PROJECT_ID',
|
||||
'language' => 'fr',
|
||||
'data' => json_encode([
|
||||
[
|
||||
'term' => 'a',
|
||||
'context' => 'messages',
|
||||
'translation' => ['content' => 'trans_fr_a'],
|
||||
],
|
||||
[
|
||||
'term' => 'post.num_comments',
|
||||
'context' => 'validators',
|
||||
'translation' => ['content' => '{count, plural, one {# commentaire} other {# commentaires}}'],
|
||||
],
|
||||
]),
|
||||
]), $options['body']);
|
||||
|
||||
return $successResponse;
|
||||
},
|
||||
];
|
||||
|
||||
$translatorBag = new TranslatorBag();
|
||||
$translatorBag->addCatalogue(new MessageCatalogue('en', [
|
||||
'messages' => ['a' => 'trans_en_a'],
|
||||
'validators' => ['post.num_comments' => '{count, plural, one {# comment} other {# comments}}'],
|
||||
]));
|
||||
$translatorBag->addCatalogue(new MessageCatalogue('fr', [
|
||||
'messages' => ['a' => 'trans_fr_a'],
|
||||
'validators' => ['post.num_comments' => '{count, plural, one {# commentaire} other {# commentaires}}'],
|
||||
]));
|
||||
|
||||
$provider = $this->createProvider(
|
||||
new MockHttpClient($responses, 'https://api.poeditor.com/v2/'),
|
||||
$this->getLoader(),
|
||||
$this->getLogger(),
|
||||
$this->getDefaultLocale(),
|
||||
'api.poeditor.com'
|
||||
);
|
||||
|
||||
$provider->write($translatorBag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getResponsesForOneLocaleAndOneDomain
|
||||
*/
|
||||
public function testReadForOneLocaleAndOneDomain(string $locale, string $domain, string $responseContent, TranslatorBag $expectedTranslatorBag)
|
||||
{
|
||||
$loader = $this->getLoader();
|
||||
$loader->expects($this->once())
|
||||
->method('load')
|
||||
->willReturn((new XliffFileLoader())->load($responseContent, $locale, $domain));
|
||||
|
||||
$responses = [
|
||||
new MockResponse(json_encode([
|
||||
'response' => [
|
||||
'status' => 'success',
|
||||
'code' => '200',
|
||||
'message' => 'OK',
|
||||
],
|
||||
'result' => [
|
||||
'url' => 'https://api.poeditor.com/v2/download/file/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
],
|
||||
])),
|
||||
new MockResponse($responseContent),
|
||||
];
|
||||
|
||||
$provider = $this->createProvider(
|
||||
new MockHttpClient($responses, 'https://api.poeditor.com/v2/'),
|
||||
$loader,
|
||||
$this->getLogger(),
|
||||
$this->getDefaultLocale(),
|
||||
'api.poeditor.com'
|
||||
);
|
||||
|
||||
$translatorBag = $provider->read([$domain], [$locale]);
|
||||
// We don't want to assert equality of metadata here, due to the ArrayLoader usage.
|
||||
foreach ($translatorBag->getCatalogues() as $catalogue) {
|
||||
$catalogue->deleteMetadata('', '');
|
||||
}
|
||||
|
||||
$this->assertEquals($expectedTranslatorBag->getCatalogues(), $translatorBag->getCatalogues());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getResponsesForManyLocalesAndManyDomains
|
||||
*/
|
||||
public function testReadForManyLocalesAndManyDomains(array $locales, array $domains, array $responseContents, TranslatorBag $expectedTranslatorBag)
|
||||
{
|
||||
$exportResponses = $downloadResponses = [];
|
||||
$consecutiveLoadArguments = [];
|
||||
$consecutiveLoadReturns = [];
|
||||
foreach ($locales as $locale) {
|
||||
foreach ($domains as $domain) {
|
||||
$exportResponses[] = new MockResponse(json_encode([
|
||||
'response' => [
|
||||
'status' => 'success',
|
||||
'code' => '200',
|
||||
'message' => 'OK',
|
||||
],
|
||||
'result' => [
|
||||
'url' => 'https://api.poeditor.com/v2/download/file/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
],
|
||||
]));
|
||||
$downloadResponses[] = new MockResponse($responseContents[$locale][$domain]);
|
||||
$consecutiveLoadArguments[] = [$responseContents[$locale][$domain], $locale, $domain];
|
||||
$consecutiveLoadReturns[] = (new XliffFileLoader())->load($responseContents[$locale][$domain], $locale, $domain);
|
||||
}
|
||||
}
|
||||
|
||||
$loader = $this->getLoader();
|
||||
$loader->expects($this->exactly(\count($consecutiveLoadArguments)))
|
||||
->method('load')
|
||||
->withConsecutive(...$consecutiveLoadArguments)
|
||||
->willReturnOnConsecutiveCalls(...$consecutiveLoadReturns);
|
||||
|
||||
$provider = $this->createProvider(
|
||||
new MockHttpClient(array_merge($exportResponses, $downloadResponses), 'https://api.poeditor.com/v2/'),
|
||||
$loader,
|
||||
$this->getLogger(),
|
||||
$this->getDefaultLocale(),
|
||||
'api.poeditor.com'
|
||||
);
|
||||
|
||||
$translatorBag = $provider->read($domains, $locales);
|
||||
// We don't want to assert equality of metadata here, due to the ArrayLoader usage.
|
||||
foreach ($translatorBag->getCatalogues() as $catalogue) {
|
||||
$catalogue->deleteMetadata('', '');
|
||||
}
|
||||
|
||||
$this->assertEquals($expectedTranslatorBag->getCatalogues(), $translatorBag->getCatalogues());
|
||||
}
|
||||
|
||||
public function testDeleteProcess()
|
||||
{
|
||||
$successResponse = new MockResponse(json_encode([
|
||||
'response' => [
|
||||
'status' => 'success',
|
||||
'code' => '200',
|
||||
'message' => 'OK',
|
||||
],
|
||||
]));
|
||||
|
||||
$translatorBag = new TranslatorBag();
|
||||
$translatorBag->addCatalogue(new MessageCatalogue('en', [
|
||||
'messages' => ['a' => 'trans_en_a'],
|
||||
'validators' => ['post.num_comments' => '{count, plural, one {# comment} other {# comments}}'],
|
||||
]));
|
||||
$translatorBag->addCatalogue(new MessageCatalogue('fr', [
|
||||
'messages' => ['a' => 'trans_fr_a'],
|
||||
'validators' => ['post.num_comments' => '{count, plural, one {# commentaire} other {# commentaires}}'],
|
||||
]));
|
||||
|
||||
$provider = $this->createProvider(
|
||||
new MockHttpClient(function (string $method, string $url, array $options = []) use ($successResponse): MockResponse {
|
||||
$this->assertSame('POST', $method);
|
||||
$this->assertSame(http_build_query([
|
||||
'api_token' => 'API_KEY',
|
||||
'id' => 'PROJECT_ID',
|
||||
'data' => json_encode([
|
||||
[
|
||||
'term' => 'a',
|
||||
'context' => 'messages',
|
||||
],
|
||||
[
|
||||
'term' => 'post.num_comments',
|
||||
'context' => 'validators',
|
||||
],
|
||||
]),
|
||||
]), $options['body']);
|
||||
|
||||
return $successResponse;
|
||||
}, 'https://api.poeditor.com/v2/'),
|
||||
$this->getLoader(),
|
||||
$this->getLogger(),
|
||||
$this->getDefaultLocale(),
|
||||
'api.poeditor.com'
|
||||
);
|
||||
|
||||
$provider->delete($translatorBag);
|
||||
}
|
||||
|
||||
public function getResponsesForOneLocaleAndOneDomain(): \Generator
|
||||
{
|
||||
$arrayLoader = new ArrayLoader();
|
||||
|
||||
$expectedTranslatorBagEn = new TranslatorBag();
|
||||
$expectedTranslatorBagEn->addCatalogue($arrayLoader->load([
|
||||
'index.hello' => 'Hello',
|
||||
'index.greetings' => 'Welcome, {firstname}!',
|
||||
], 'en'));
|
||||
|
||||
yield ['en', 'messages', <<<'XLIFF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" target-language="en" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="index.hello" datatype="html">
|
||||
<source>index.hello</source>
|
||||
<target>Hello</target>
|
||||
<note priority="1" from="meaning">index.hello</note>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">index.hello</context>
|
||||
<context context-type="linenumber"/>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="index.greetings" datatype="html">
|
||||
<source>index.greetings</source>
|
||||
<target>Welcome, {firstname}!</target>
|
||||
<note priority="1" from="meaning">index.greetings</note>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">index.greetings</context>
|
||||
<context context-type="linenumber"/>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
XLIFF
|
||||
,
|
||||
$expectedTranslatorBagEn,
|
||||
];
|
||||
|
||||
$expectedTranslatorBagFr = new TranslatorBag();
|
||||
$expectedTranslatorBagFr->addCatalogue($arrayLoader->load([
|
||||
'index.hello' => 'Bonjour',
|
||||
'index.greetings' => 'Bienvenue, {firstname} !',
|
||||
], 'fr'));
|
||||
|
||||
yield ['fr', 'messages', <<<'XLIFF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="index.hello" datatype="html">
|
||||
<source>index.hello</source>
|
||||
<target>Bonjour</target>
|
||||
<note priority="1" from="meaning">index.hello</note>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">index.hello</context>
|
||||
<context context-type="linenumber"/>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="index.greetings" datatype="html">
|
||||
<source>index.greetings</source>
|
||||
<target>Bienvenue, {firstname} !</target>
|
||||
<note priority="1" from="meaning">index.greetings</note>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">index.greetings</context>
|
||||
<context context-type="linenumber"/>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
XLIFF
|
||||
,
|
||||
$expectedTranslatorBagFr,
|
||||
];
|
||||
}
|
||||
|
||||
public function getResponsesForManyLocalesAndManyDomains(): \Generator
|
||||
{
|
||||
$arrayLoader = new ArrayLoader();
|
||||
|
||||
$expectedTranslatorBag = new TranslatorBag();
|
||||
$expectedTranslatorBag->addCatalogue($arrayLoader->load([
|
||||
'index.hello' => 'Hello',
|
||||
'index.greetings' => 'Welcome, {firstname}!',
|
||||
], 'en'));
|
||||
$expectedTranslatorBag->addCatalogue($arrayLoader->load([
|
||||
'index.hello' => 'Bonjour',
|
||||
'index.greetings' => 'Bienvenue, {firstname} !',
|
||||
], 'fr'));
|
||||
$expectedTranslatorBag->addCatalogue($arrayLoader->load([
|
||||
'firstname.error' => 'Firstname must contains only letters.',
|
||||
'lastname.error' => 'Lastname must contains only letters.',
|
||||
], 'en', 'validators'));
|
||||
$expectedTranslatorBag->addCatalogue($arrayLoader->load([
|
||||
'firstname.error' => 'Le prénom ne peut contenir que des lettres.',
|
||||
'lastname.error' => 'Le nom de famille ne peut contenir que des lettres.',
|
||||
], 'fr', 'validators'));
|
||||
|
||||
yield [
|
||||
['en', 'fr'],
|
||||
['messages', 'validators'],
|
||||
[
|
||||
'en' => [
|
||||
'messages' => <<<'XLIFF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" target-language="en" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="index.hello" datatype="html">
|
||||
<source>index.hello</source>
|
||||
<target>Hello</target>
|
||||
<note priority="1" from="meaning">index.hello</note>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">index.hello</context>
|
||||
<context context-type="linenumber"/>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="index.greetings" datatype="html">
|
||||
<source>index.greetings</source>
|
||||
<target>Welcome, {firstname}!</target>
|
||||
<note priority="1" from="meaning">index.greetings</note>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">index.greetings</context>
|
||||
<context context-type="linenumber"/>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
XLIFF
|
||||
,
|
||||
'validators' => <<<'XLIFF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" target-language="en" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="firstname.error" datatype="html">
|
||||
<source>firstname.error</source>
|
||||
<target>Firstname must contains only letters.</target>
|
||||
<note priority="1" from="meaning">firstname.error</note>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">firstname.error</context>
|
||||
<context context-type="linenumber"/>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="lastname.error" datatype="html">
|
||||
<source>lastname.error</source>
|
||||
<target>Lastname must contains only letters.</target>
|
||||
<note priority="1" from="meaning">lastname.error</note>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">lastname.error</context>
|
||||
<context context-type="linenumber"/>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
XLIFF
|
||||
,
|
||||
],
|
||||
'fr' => [
|
||||
'messages' => <<<'XLIFF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="index.hello" datatype="html">
|
||||
<source>index.hello</source>
|
||||
<target>Bonjour</target>
|
||||
<note priority="1" from="meaning">index.hello</note>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">index.hello</context>
|
||||
<context context-type="linenumber"/>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="index.greetings" datatype="html">
|
||||
<source>index.greetings</source>
|
||||
<target>Bienvenue, {firstname} !</target>
|
||||
<note priority="1" from="meaning">index.greetings</note>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">index.greetings</context>
|
||||
<context context-type="linenumber"/>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
XLIFF
|
||||
,
|
||||
'validators' => <<<'XLIFF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="firstname.error" datatype="html">
|
||||
<source>firstname.error</source>
|
||||
<target>Le prénom ne peut contenir que des lettres.</target>
|
||||
<note priority="1" from="meaning">firstname.error</note>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">firstname.error</context>
|
||||
<context context-type="linenumber"/>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="lastname.error" datatype="html">
|
||||
<source>lastname.error</source>
|
||||
<target>Le nom de famille ne peut contenir que des lettres.</target>
|
||||
<note priority="1" from="meaning">lastname.error</note>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">lastname.error</context>
|
||||
<context context-type="linenumber"/>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
XLIFF
|
||||
,
|
||||
],
|
||||
],
|
||||
$expectedTranslatorBag,
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "symfony/po-editor-translation-provider",
|
||||
"type": "symfony-bridge",
|
||||
"description": "Symfony PoEditor Translation Provider Bridge",
|
||||
"keywords": ["poeditor", "translation", "provider"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mathieu Santostefano",
|
||||
"homepage": "https://github.com/welcomattic"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/http-client": "^5.3",
|
||||
"symfony/translation": "^5.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/config": "^4.4|^5.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Translation\\Bridge\\PoEditor\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
bootstrap="vendor/autoload.php"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Symfony PoEditor Translation Provider Bridge Test Suite">
|
||||
<directory>./Tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory>./</directory>
|
||||
<exclude>
|
||||
<directory>./Resources</directory>
|
||||
<directory>./Tests</directory>
|
||||
<directory>./vendor</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
@ -5,6 +5,8 @@ CHANGELOG
|
||||
---
|
||||
|
||||
* Add `translation:pull` and `translation:push` commands to manage translations with third-party providers
|
||||
* Add `TranslatorBagInterface::getCatalogues` method
|
||||
* Add support to load XLIFF string in `XliffFileLoader`
|
||||
|
||||
5.2.0
|
||||
-----
|
||||
|
@ -25,6 +25,10 @@ class UnsupportedSchemeException extends LogicException
|
||||
'class' => Bridge\Loco\LocoProviderFactory::class,
|
||||
'package' => 'symfony/loco-translation-provider',
|
||||
],
|
||||
'poeditor' => [
|
||||
'class' => Bridge\PoEditor\PoEditorProviderFactory::class,
|
||||
'package' => 'symfony/po-editor-translation-provider',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(Dsn $dsn, string $name = null, array $supported = [])
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\Translation\Test;
|
||||
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
@ -54,11 +55,17 @@ abstract class ProviderTestCase extends TestCase
|
||||
return $this->client ?? $this->client = new MockHttpClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LoaderInterface&MockObject
|
||||
*/
|
||||
protected function getLoader(): LoaderInterface
|
||||
{
|
||||
return $this->loader ?? $this->loader = $this->createMock(LoaderInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LoaderInterface&MockObject
|
||||
*/
|
||||
protected function getLogger(): LoggerInterface
|
||||
{
|
||||
return $this->logger ?? $this->logger = $this->createMock(LoggerInterface::class);
|
||||
@ -69,6 +76,9 @@ abstract class ProviderTestCase extends TestCase
|
||||
return $this->defaultLocale ?? $this->defaultLocale = 'en';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LoaderInterface&MockObject
|
||||
*/
|
||||
protected function getXliffFileDumper(): XliffFileDumper
|
||||
{
|
||||
return $this->xliffFileDumper ?? $this->xliffFileDumper = $this->createMock(XliffFileDumper::class);
|
||||
|
Reference in New Issue
Block a user