Added Lokalise Provider
This commit is contained in:
parent
af19b6bec2
commit
022d8285f3
@ -171,6 +171,7 @@ use Symfony\Component\String\LazyString;
|
|||||||
use Symfony\Component\String\Slugger\SluggerInterface;
|
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||||
use Symfony\Component\Translation\Bridge\Crowdin\CrowdinProviderFactory;
|
use Symfony\Component\Translation\Bridge\Crowdin\CrowdinProviderFactory;
|
||||||
use Symfony\Component\Translation\Bridge\Loco\LocoProviderFactory;
|
use Symfony\Component\Translation\Bridge\Loco\LocoProviderFactory;
|
||||||
|
use Symfony\Component\Translation\Bridge\Lokalise\LokaliseProviderFactory;
|
||||||
use Symfony\Component\Translation\Bridge\PoEditor\PoEditorProviderFactory;
|
use Symfony\Component\Translation\Bridge\PoEditor\PoEditorProviderFactory;
|
||||||
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
|
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
|
||||||
use Symfony\Component\Translation\PseudoLocalizationTranslator;
|
use Symfony\Component\Translation\PseudoLocalizationTranslator;
|
||||||
@ -1345,6 +1346,7 @@ class FrameworkExtension extends Extension
|
|||||||
$classToServices = [
|
$classToServices = [
|
||||||
CrowdinProviderFactory::class => 'translation.provider_factory.crowdin',
|
CrowdinProviderFactory::class => 'translation.provider_factory.crowdin',
|
||||||
LocoProviderFactory::class => 'translation.provider_factory.loco',
|
LocoProviderFactory::class => 'translation.provider_factory.loco',
|
||||||
|
LokaliseProviderFactory::class => 'translation.provider_factory.lokalise',
|
||||||
PoEditorProviderFactory::class => 'translation.provider_factory.poeditor',
|
PoEditorProviderFactory::class => 'translation.provider_factory.poeditor',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
|||||||
|
|
||||||
use Symfony\Component\Translation\Bridge\Crowdin\CrowdinProviderFactory;
|
use Symfony\Component\Translation\Bridge\Crowdin\CrowdinProviderFactory;
|
||||||
use Symfony\Component\Translation\Bridge\Loco\LocoProviderFactory;
|
use Symfony\Component\Translation\Bridge\Loco\LocoProviderFactory;
|
||||||
|
use Symfony\Component\Translation\Bridge\Lokalise\LokaliseProviderFactory;
|
||||||
use Symfony\Component\Translation\Bridge\PoEditor\PoEditorProviderFactory;
|
use Symfony\Component\Translation\Bridge\PoEditor\PoEditorProviderFactory;
|
||||||
use Symfony\Component\Translation\Provider\NullProviderFactory;
|
use Symfony\Component\Translation\Provider\NullProviderFactory;
|
||||||
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
|
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
|
||||||
@ -54,6 +55,15 @@ return static function (ContainerConfigurator $container) {
|
|||||||
])
|
])
|
||||||
->tag('translation.provider_factory')
|
->tag('translation.provider_factory')
|
||||||
|
|
||||||
|
->set('translation.provider_factory.lokalise', LokaliseProviderFactory::class)
|
||||||
|
->args([
|
||||||
|
service('http_client'),
|
||||||
|
service('logger'),
|
||||||
|
param('kernel.default_locale'),
|
||||||
|
service('translation.loader.xliff'),
|
||||||
|
])
|
||||||
|
->tag('translation.provider_factory')
|
||||||
|
|
||||||
->set('translation.provider_factory.poeditor', PoEditorProviderFactory::class)
|
->set('translation.provider_factory.poeditor', PoEditorProviderFactory::class)
|
||||||
->args([
|
->args([
|
||||||
service('http_client'),
|
service('http_client'),
|
||||||
|
4
src/Symfony/Component/Translation/Bridge/Lokalise/.gitattributes
vendored
Normal file
4
src/Symfony/Component/Translation/Bridge/Lokalise/.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/Lokalise/.gitignore
vendored
Normal file
3
src/Symfony/Component/Translation/Bridge/Lokalise/.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/Lokalise/LICENSE
Normal file
19
src/Symfony/Component/Translation/Bridge/Lokalise/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,343 @@
|
|||||||
|
<?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\Lokalise;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Translation\Exception\ProviderException;
|
||||||
|
use Symfony\Component\Translation\Loader\LoaderInterface;
|
||||||
|
use Symfony\Component\Translation\MessageCatalogueInterface;
|
||||||
|
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 Lokalise:
|
||||||
|
* * Filenames refers to Symfony's translation domains;
|
||||||
|
* * Keys refers to Symfony's translation keys;
|
||||||
|
* * Translations refers to Symfony's translated messages
|
||||||
|
*
|
||||||
|
* @experimental in 5.3
|
||||||
|
*/
|
||||||
|
final class LokaliseProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
private $client;
|
||||||
|
private $loader;
|
||||||
|
private $logger;
|
||||||
|
private $defaultLocale;
|
||||||
|
private $endpoint;
|
||||||
|
|
||||||
|
public function __construct(HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, string $defaultLocale, string $endpoint)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
$this->loader = $loader;
|
||||||
|
$this->logger = $logger;
|
||||||
|
$this->defaultLocale = $defaultLocale;
|
||||||
|
$this->endpoint = $endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return sprintf('lokalise://%s', $this->endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* Lokalise API recommends sending payload in chunks of up to 500 keys per request.
|
||||||
|
*
|
||||||
|
* @see https://app.lokalise.com/api2docs/curl/#transition-create-keys-post
|
||||||
|
*/
|
||||||
|
public function write(TranslatorBagInterface $translatorBag): void
|
||||||
|
{
|
||||||
|
$defaultCatalogue = $translatorBag->getCatalogue($this->defaultLocale);
|
||||||
|
|
||||||
|
if (!$defaultCatalogue) {
|
||||||
|
$defaultCatalogue = $translatorBag->getCatalogues()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ensureAllLocalesAreCreated($translatorBag);
|
||||||
|
$existingKeysByDomain = [];
|
||||||
|
|
||||||
|
foreach ($defaultCatalogue->getDomains() as $domain) {
|
||||||
|
if (!\array_key_exists($domain, $existingKeysByDomain)) {
|
||||||
|
$existingKeysByDomain[$domain] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingKeysByDomain[$domain] += $this->getKeysIds(array_keys($defaultCatalogue->all($domain)), $domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
$keysToCreate = $createdKeysByDomain = [];
|
||||||
|
|
||||||
|
foreach ($existingKeysByDomain as $domain => $existingKeys) {
|
||||||
|
$allKeysForDomain = array_keys($defaultCatalogue->all($domain));
|
||||||
|
foreach (array_keys($existingKeys) as $keyName) {
|
||||||
|
unset($allKeysForDomain[$keyName]);
|
||||||
|
}
|
||||||
|
$keysToCreate[$domain] = $allKeysForDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($keysToCreate as $domain => $keys) {
|
||||||
|
$createdKeysByDomain[$domain] = $this->createKeys($keys, $domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->updateTranslations(array_merge($createdKeysByDomain, $existingKeysByDomain), $translatorBag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read(array $domains, array $locales): TranslatorBag
|
||||||
|
{
|
||||||
|
$translatorBag = new TranslatorBag();
|
||||||
|
$translations = $this->exportFiles($locales, $domains);
|
||||||
|
|
||||||
|
foreach ($translations as $locale => $files) {
|
||||||
|
foreach ($files as $filename => $content) {
|
||||||
|
$translatorBag->addCatalogue($this->loader->load($content['content'], $locale, str_replace('.xliff', '', $filename)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $translatorBag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(TranslatorBagInterface $translatorBag): void
|
||||||
|
{
|
||||||
|
$catalogue = $translatorBag->getCatalogue($this->defaultLocale);
|
||||||
|
|
||||||
|
if (!$catalogue) {
|
||||||
|
$catalogue = $translatorBag->getCatalogues()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$keysIds = [];
|
||||||
|
|
||||||
|
foreach ($catalogue->getDomains() as $domain) {
|
||||||
|
$keysToDelete = [];
|
||||||
|
foreach (array_keys($catalogue->all($domain)) as $key) {
|
||||||
|
$keysToDelete[] = $key;
|
||||||
|
}
|
||||||
|
$keysIds += $this->getKeysIds($keysToDelete, $domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->client->request('DELETE', 'keys', [
|
||||||
|
'json' => ['keys' => array_values($keysIds)],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
throw new ProviderException(sprintf('Unable to delete keys from Lokalise: "%s".', $response->getContent(false)), $response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://app.lokalise.com/api2docs/curl/#transition-download-files-post
|
||||||
|
*/
|
||||||
|
private function exportFiles(array $locales, array $domains): array
|
||||||
|
{
|
||||||
|
$response = $this->client->request('POST', 'files/export', [
|
||||||
|
'json' => [
|
||||||
|
'format' => 'symfony_xliff',
|
||||||
|
'original_filenames' => true,
|
||||||
|
'directory_prefix' => '%LANG_ISO%',
|
||||||
|
'filter_langs' => array_values($locales),
|
||||||
|
'filter_filenames' => array_map([$this, 'getLokaliseFilenameFromDomain'], $domains),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$responseContent = $response->toArray(false);
|
||||||
|
|
||||||
|
if (406 === $response->getStatusCode()
|
||||||
|
&& 'No keys found with specified filenames.' === $responseContent['error']['message']
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
throw new ProviderException(sprintf('Unable to export translations from Lokalise: "%s".', $response->getContent(false)), $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $responseContent['files'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createKeys(array $keys, string $domain): array
|
||||||
|
{
|
||||||
|
$keysToCreate = [];
|
||||||
|
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
$keysToCreate[] = [
|
||||||
|
'key_name' => $key,
|
||||||
|
'platforms' => ['web'],
|
||||||
|
'filenames' => [
|
||||||
|
'web' => $this->getLokaliseFilenameFromDomain($domain),
|
||||||
|
// There is a bug in Lokalise with "Per platform key names" option enabled,
|
||||||
|
// we need to provide a filename for all platforms.
|
||||||
|
'ios' => null,
|
||||||
|
'android' => null,
|
||||||
|
'other' => null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$chunks = array_chunk($keysToCreate, 500);
|
||||||
|
$responses = [];
|
||||||
|
|
||||||
|
foreach ($chunks as $chunk) {
|
||||||
|
$responses[] = $this->client->request('POST', 'keys', [
|
||||||
|
'json' => ['keys' => $chunk],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$createdKeys = [];
|
||||||
|
|
||||||
|
foreach ($responses as $response) {
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
$this->logger->error(sprintf('Unable to create keys to Lokalise: "%s".', $response->getContent(false)));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$createdKeys = array_reduce($response->toArray(false)['keys'], function ($carry, array $keyItem) {
|
||||||
|
$carry[$keyItem['key_name']['web']] = $keyItem['key_id'];
|
||||||
|
|
||||||
|
return $carry;
|
||||||
|
}, $createdKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $createdKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translations will be created for keys without existing translations.
|
||||||
|
* Translations will be updated for keys with existing translations.
|
||||||
|
*/
|
||||||
|
private function updateTranslations(array $keysByDomain, TranslatorBagInterface $translatorBag)
|
||||||
|
{
|
||||||
|
$keysToUpdate = [];
|
||||||
|
|
||||||
|
foreach ($keysByDomain as $domain => $keys) {
|
||||||
|
foreach ($keys as $keyName => $keyId) {
|
||||||
|
$keysToUpdate[] = [
|
||||||
|
'key_id' => $keyId,
|
||||||
|
'platforms' => ['web'],
|
||||||
|
'filenames' => [
|
||||||
|
'web' => $this->getLokaliseFilenameFromDomain($domain),
|
||||||
|
'ios' => null,
|
||||||
|
'android' => null,
|
||||||
|
'other' => null,
|
||||||
|
],
|
||||||
|
'translations' => array_reduce($translatorBag->getCatalogues(), function ($carry, MessageCatalogueInterface $catalogue) use ($keyName, $domain) {
|
||||||
|
// Message could be not found because the catalogue is empty.
|
||||||
|
// We must not send the key in place of the message to avoid wrong message update on the provider.
|
||||||
|
if ($catalogue->get($keyName, $domain) !== $keyName) {
|
||||||
|
$carry[] = [
|
||||||
|
'language_iso' => $catalogue->getLocale(),
|
||||||
|
'translation' => $catalogue->get($keyName, $domain),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $carry;
|
||||||
|
}, []),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$chunks = array_chunk($keysToUpdate, 500);
|
||||||
|
$responses = [];
|
||||||
|
|
||||||
|
foreach ($chunks as $chunk) {
|
||||||
|
$responses[] = $this->client->request('PUT', 'keys', [
|
||||||
|
'json' => ['keys' => $chunk],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($responses as $response) {
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
$this->logger->error(sprintf('Unable to create/update translations to Lokalise: "%s".', $response->getContent(false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getKeysIds(array $keys, string $domain): array
|
||||||
|
{
|
||||||
|
$response = $this->client->request('GET', 'keys', [
|
||||||
|
'query' => [
|
||||||
|
'filter_keys' => implode(',', $keys),
|
||||||
|
'filter_filenames' => $this->getLokaliseFilenameFromDomain($domain),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
$this->logger->error(sprintf('Unable to get keys ids from Lokalise: "%s".', $response->getContent(false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_reduce($response->toArray(false)['keys'], function ($carry, array $keyItem) {
|
||||||
|
$carry[$keyItem['key_name']['web']] = $keyItem['key_id'];
|
||||||
|
|
||||||
|
return $carry;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureAllLocalesAreCreated(TranslatorBagInterface $translatorBag)
|
||||||
|
{
|
||||||
|
$providerLanguages = $this->getLanguages();
|
||||||
|
$missingLanguages = array_reduce($translatorBag->getCatalogues(), function ($carry, $catalogue) use ($providerLanguages) {
|
||||||
|
if (!\in_array($catalogue->getLocale(), $providerLanguages)) {
|
||||||
|
$carry[] = $catalogue->getLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $carry;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if ($missingLanguages) {
|
||||||
|
$this->createLanguages($missingLanguages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLanguages(): array
|
||||||
|
{
|
||||||
|
$response = $this->client->request('GET', 'languages');
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
$this->logger->error(sprintf('Unable to get languages from Lokalise: "%s".', $response->getContent(false)));
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$responseContent = $response->toArray(false);
|
||||||
|
|
||||||
|
if (\array_key_exists('languages', $responseContent)) {
|
||||||
|
return array_map(function ($language) {
|
||||||
|
return $language['lang_iso'];
|
||||||
|
}, $responseContent['languages']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createLanguages(array $languages): void
|
||||||
|
{
|
||||||
|
$response = $this->client->request('POST', 'languages', [
|
||||||
|
'json' => [
|
||||||
|
'languages' => array_map(function ($language) {
|
||||||
|
return ['lang_iso' => $language];
|
||||||
|
}, $languages),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
$this->logger->error(sprintf('Unable to create languages on Lokalise: "%s".', $response->getContent(false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLokaliseFilenameFromDomain(string $domain): string
|
||||||
|
{
|
||||||
|
return sprintf('%s.xliff', $domain);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
<?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\Lokalise;
|
||||||
|
|
||||||
|
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 LokaliseProviderFactory extends AbstractProviderFactory
|
||||||
|
{
|
||||||
|
private const HOST = 'api.lokalise.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 LokaliseProvider
|
||||||
|
*/
|
||||||
|
public function create(Dsn $dsn): ProviderInterface
|
||||||
|
{
|
||||||
|
if ('lokalise' !== $dsn->getScheme()) {
|
||||||
|
throw new UnsupportedSchemeException($dsn, 'lokalise', $this->getSupportedSchemes());
|
||||||
|
}
|
||||||
|
|
||||||
|
$endpoint = sprintf('%s%s', 'default' === $dsn->getHost() ? self::HOST : $dsn->getHost(), $dsn->getPort() ? ':'.$dsn->getPort() : '');
|
||||||
|
$client = $this->client->withOptions([
|
||||||
|
'base_uri' => sprintf('https://%sprojects/%s/api2/', $endpoint, $this->getUser($dsn)),
|
||||||
|
'headers' => [
|
||||||
|
'X-Api-Token' => $this->getPassword($dsn),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new LokaliseProvider($client, $this->loader, $this->logger, $this->defaultLocale, $endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getSupportedSchemes(): array
|
||||||
|
{
|
||||||
|
return ['lokalise'];
|
||||||
|
}
|
||||||
|
}
|
28
src/Symfony/Component/Translation/Bridge/Lokalise/README.md
Normal file
28
src/Symfony/Component/Translation/Bridge/Lokalise/README.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
Lokalise Translation Provider
|
||||||
|
=============================
|
||||||
|
|
||||||
|
Provides Lokalise integration for Symfony Translation.
|
||||||
|
|
||||||
|
DSN example
|
||||||
|
-----------
|
||||||
|
|
||||||
|
```
|
||||||
|
// .env file
|
||||||
|
LOKALISE_DSN=lokalise://PROJECT_ID:API_KEY@default
|
||||||
|
```
|
||||||
|
|
||||||
|
where:
|
||||||
|
- `PROJECT_ID` is your Lokalise Project ID
|
||||||
|
- `API_KEY` is your Lokalise API key
|
||||||
|
|
||||||
|
Go to the Project Settings in Lokalise to find the Project ID.
|
||||||
|
|
||||||
|
[Generate an API key on Lokalise](https://app.lokalise.com/api2docs/curl/#resource-authentication)
|
||||||
|
|
||||||
|
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\Lokalise\Tests;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\Bridge\Lokalise\LokaliseProviderFactory;
|
||||||
|
use Symfony\Component\Translation\Provider\ProviderFactoryInterface;
|
||||||
|
use Symfony\Component\Translation\Test\ProviderFactoryTestCase;
|
||||||
|
|
||||||
|
class LokaliseProviderFactoryTest extends ProviderFactoryTestCase
|
||||||
|
{
|
||||||
|
public function supportsProvider(): iterable
|
||||||
|
{
|
||||||
|
yield [true, 'lokalise://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 [
|
||||||
|
'lokalise://api.lokalise.com',
|
||||||
|
'lokalise://PROJECT_ID:API_KEY@default',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function incompleteDsnProvider(): iterable
|
||||||
|
{
|
||||||
|
yield ['lokalise://default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createFactory(): ProviderFactoryInterface
|
||||||
|
{
|
||||||
|
return new LokaliseProviderFactory($this->getClient(), $this->getLogger(), $this->getDefaultLocale(), $this->getLoader());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,584 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Symfony\Component\Translation\Bridge\Loco\Tests;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpClient\MockHttpClient;
|
||||||
|
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||||
|
use Symfony\Component\Translation\Bridge\Lokalise\LokaliseProvider;
|
||||||
|
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;
|
||||||
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
class LokaliseProviderTest extends ProviderTestCase
|
||||||
|
{
|
||||||
|
public function createProvider(HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, string $defaultLocale, string $endpoint): ProviderInterface
|
||||||
|
{
|
||||||
|
return new LokaliseProvider($client, $loader, $logger, $defaultLocale, $endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toStringProvider(): iterable
|
||||||
|
{
|
||||||
|
yield [
|
||||||
|
$this->createProvider($this->getClient()->withOptions([
|
||||||
|
'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/',
|
||||||
|
'headers' => ['X-Api-Token' => 'API_KEY'],
|
||||||
|
]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.com'),
|
||||||
|
'lokalise://api.lokalise.com',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$this->createProvider($this->getClient()->withOptions([
|
||||||
|
'base_uri' => 'https://example.com',
|
||||||
|
'headers' => ['X-Api-Token' => 'API_KEY'],
|
||||||
|
]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'example.com'),
|
||||||
|
'lokalise://example.com',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$this->createProvider($this->getClient()->withOptions([
|
||||||
|
'base_uri' => 'https://example.com:99',
|
||||||
|
'headers' => ['X-Api-Token' => 'API_KEY'],
|
||||||
|
]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'example.com:99'),
|
||||||
|
'lokalise://example.com:99',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCompleteWriteProcess()
|
||||||
|
{
|
||||||
|
$getLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface {
|
||||||
|
$this->assertSame('GET', $method);
|
||||||
|
$this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url);
|
||||||
|
|
||||||
|
return new MockResponse(json_encode(['languages' => []]));
|
||||||
|
};
|
||||||
|
|
||||||
|
$createLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface {
|
||||||
|
$expectedBody = json_encode([
|
||||||
|
'languages' => [
|
||||||
|
['lang_iso' => 'en'],
|
||||||
|
['lang_iso' => 'fr'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('POST', $method);
|
||||||
|
$this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url);
|
||||||
|
$this->assertSame($expectedBody, $options['body']);
|
||||||
|
|
||||||
|
return new MockResponse();
|
||||||
|
};
|
||||||
|
|
||||||
|
$getKeysIdsForMessagesDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface {
|
||||||
|
$expectedQuery = [
|
||||||
|
'filter_keys' => 'a',
|
||||||
|
'filter_filenames' => 'messages.xliff',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertSame('GET', $method);
|
||||||
|
$this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/keys?'.http_build_query($expectedQuery), $url);
|
||||||
|
$this->assertSame($expectedQuery, $options['query']);
|
||||||
|
|
||||||
|
return new MockResponse(json_encode(['keys' => []]));
|
||||||
|
};
|
||||||
|
|
||||||
|
$getKeysIdsForValidatorsDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface {
|
||||||
|
$expectedQuery = [
|
||||||
|
'filter_keys' => 'post.num_comments',
|
||||||
|
'filter_filenames' => 'validators.xliff',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertSame('GET', $method);
|
||||||
|
$this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/keys?'.http_build_query($expectedQuery), $url);
|
||||||
|
$this->assertSame($expectedQuery, $options['query']);
|
||||||
|
|
||||||
|
return new MockResponse(json_encode(['keys' => []]));
|
||||||
|
};
|
||||||
|
|
||||||
|
$createKeysForMessagesDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface {
|
||||||
|
$expectedBody = json_encode([
|
||||||
|
'keys' => [
|
||||||
|
[
|
||||||
|
'key_name' => 'a',
|
||||||
|
'platforms' => ['web'],
|
||||||
|
'filenames' => [
|
||||||
|
'web' => 'messages.xliff',
|
||||||
|
'ios' => null,
|
||||||
|
'android' => null,
|
||||||
|
'other' => null,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('POST', $method);
|
||||||
|
$this->assertSame($expectedBody, $options['body']);
|
||||||
|
|
||||||
|
return new MockResponse(json_encode(['keys' => [
|
||||||
|
[
|
||||||
|
'key_name' => ['web' => 'a'],
|
||||||
|
'key_id' => 29,
|
||||||
|
],
|
||||||
|
]]));
|
||||||
|
};
|
||||||
|
|
||||||
|
$createKeysForValidatorsDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface {
|
||||||
|
$expectedBody = json_encode([
|
||||||
|
'keys' => [
|
||||||
|
[
|
||||||
|
'key_name' => 'post.num_comments',
|
||||||
|
'platforms' => ['web'],
|
||||||
|
'filenames' => [
|
||||||
|
'web' => 'validators.xliff',
|
||||||
|
'ios' => null,
|
||||||
|
'android' => null,
|
||||||
|
'other' => null,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('POST', $method);
|
||||||
|
$this->assertSame($expectedBody, $options['body']);
|
||||||
|
|
||||||
|
return new MockResponse(json_encode(['keys' => [
|
||||||
|
[
|
||||||
|
'key_name' => ['web' => 'post.num_comments'],
|
||||||
|
'key_id' => 92,
|
||||||
|
],
|
||||||
|
]]));
|
||||||
|
};
|
||||||
|
|
||||||
|
$updateTranslationsResponse = function (string $method, string $url, array $options = []): ResponseInterface {
|
||||||
|
$expectedBody = json_encode([
|
||||||
|
'keys' => [
|
||||||
|
[
|
||||||
|
'key_id' => 29,
|
||||||
|
'platforms' => ['web'],
|
||||||
|
'filenames' => [
|
||||||
|
'web' => 'messages.xliff',
|
||||||
|
'ios' => null,
|
||||||
|
'android' => null,
|
||||||
|
'other' => null,
|
||||||
|
],
|
||||||
|
'translations' => [
|
||||||
|
[
|
||||||
|
'language_iso' => 'en',
|
||||||
|
'translation' => 'trans_en_a',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'language_iso' => 'fr',
|
||||||
|
'translation' => 'trans_fr_a',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key_id' => 92,
|
||||||
|
'platforms' => ['web'],
|
||||||
|
'filenames' => [
|
||||||
|
'web' => 'validators.xliff',
|
||||||
|
'ios' => null,
|
||||||
|
'android' => null,
|
||||||
|
'other' => null,
|
||||||
|
],
|
||||||
|
'translations' => [
|
||||||
|
[
|
||||||
|
'language_iso' => 'en',
|
||||||
|
'translation' => '{count, plural, one {# comment} other {# comments}}',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'language_iso' => 'fr',
|
||||||
|
'translation' => '{count, plural, one {# commentaire} other {# commentaires}}',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('PUT', $method);
|
||||||
|
$this->assertSame($expectedBody, $options['body']);
|
||||||
|
|
||||||
|
return new MockResponse();
|
||||||
|
};
|
||||||
|
|
||||||
|
$provider = $this->createProvider((new MockHttpClient([
|
||||||
|
$getLanguagesResponse,
|
||||||
|
$createLanguagesResponse,
|
||||||
|
$getKeysIdsForMessagesDomainResponse,
|
||||||
|
$getKeysIdsForValidatorsDomainResponse,
|
||||||
|
$createKeysForMessagesDomainResponse,
|
||||||
|
$createKeysForValidatorsDomainResponse,
|
||||||
|
$updateTranslationsResponse,
|
||||||
|
]))->withOptions([
|
||||||
|
'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/',
|
||||||
|
'headers' => ['X-Api-Token' => 'API_KEY'],
|
||||||
|
]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.com');
|
||||||
|
|
||||||
|
$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->write($translatorBag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getResponsesForOneLocaleAndOneDomain
|
||||||
|
*/
|
||||||
|
public function testReadForOneLocaleAndOneDomain(string $locale, string $domain, string $responseContent, TranslatorBag $expectedTranslatorBag)
|
||||||
|
{
|
||||||
|
$response = function (string $method, string $url, array $options = []) use ($locale, $domain, $responseContent): ResponseInterface {
|
||||||
|
$expectedBody = json_encode([
|
||||||
|
'format' => 'symfony_xliff',
|
||||||
|
'original_filenames' => true,
|
||||||
|
'directory_prefix' => '%LANG_ISO%',
|
||||||
|
'filter_langs' => [$locale],
|
||||||
|
'filter_filenames' => [$domain.'.xliff'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame('POST', $method);
|
||||||
|
$this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/files/export', $url);
|
||||||
|
$this->assertSame($expectedBody, $options['body']);
|
||||||
|
|
||||||
|
return new MockResponse(json_encode([
|
||||||
|
'files' => [
|
||||||
|
$locale => [
|
||||||
|
$domain.'.xliff' => [
|
||||||
|
'content' => $responseContent,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
};
|
||||||
|
|
||||||
|
$loader = $this->getLoader();
|
||||||
|
$loader->expects($this->once())
|
||||||
|
->method('load')
|
||||||
|
->willReturn((new XliffFileLoader())->load($responseContent, $locale, $domain));
|
||||||
|
|
||||||
|
$provider = $this->createProvider((new MockHttpClient($response))->withOptions([
|
||||||
|
'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/',
|
||||||
|
'headers' => ['X-Api-Token' => 'API_KEY'],
|
||||||
|
]), $loader, $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.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)
|
||||||
|
{
|
||||||
|
$consecutiveLoadArguments = [];
|
||||||
|
$consecutiveLoadReturns = [];
|
||||||
|
$response = new MockResponse(json_encode([
|
||||||
|
'files' => array_reduce($locales, function ($carry, $locale) use ($domains, $responseContents, &$consecutiveLoadArguments, &$consecutiveLoadReturns) {
|
||||||
|
$carry[$locale] = array_reduce($domains, function ($carry, $domain) use ($locale, $responseContents, &$consecutiveLoadArguments, &$consecutiveLoadReturns) {
|
||||||
|
$carry[$domain.'.xliff'] = [
|
||||||
|
'content' => $responseContents[$locale][$domain],
|
||||||
|
];
|
||||||
|
|
||||||
|
$consecutiveLoadArguments[] = [$responseContents[$locale][$domain], $locale, $domain];
|
||||||
|
$consecutiveLoadReturns[] = (new XliffFileLoader())->load($responseContents[$locale][$domain], $locale, $domain);
|
||||||
|
|
||||||
|
return $carry;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return $carry;
|
||||||
|
}, []),
|
||||||
|
]));
|
||||||
|
|
||||||
|
$loader = $this->getLoader();
|
||||||
|
$loader->expects($this->exactly(\count($consecutiveLoadArguments)))
|
||||||
|
->method('load')
|
||||||
|
->withConsecutive(...$consecutiveLoadArguments)
|
||||||
|
->willReturnOnConsecutiveCalls(...$consecutiveLoadReturns);
|
||||||
|
|
||||||
|
$provider = $this->createProvider((new MockHttpClient($response))->withOptions([
|
||||||
|
'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/',
|
||||||
|
'headers' => ['X-Api-Token' => 'API_KEY'],
|
||||||
|
]), $loader, $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.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('', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($locales as $locale) {
|
||||||
|
foreach ($domains as $domain) {
|
||||||
|
$this->assertEquals($expectedTranslatorBag->getCatalogue($locale)->all($domain), $translatorBag->getCatalogue($locale)->all($domain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteProcess()
|
||||||
|
{
|
||||||
|
$getKeysIdsForMessagesDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface {
|
||||||
|
$expectedQuery = [
|
||||||
|
'filter_keys' => 'a',
|
||||||
|
'filter_filenames' => 'messages.xliff',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertSame('GET', $method);
|
||||||
|
$this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/keys?'.http_build_query($expectedQuery), $url);
|
||||||
|
$this->assertSame($expectedQuery, $options['query']);
|
||||||
|
|
||||||
|
return new MockResponse(json_encode(['keys' => [
|
||||||
|
[
|
||||||
|
'key_name' => ['web' => 'a'],
|
||||||
|
'key_id' => 29,
|
||||||
|
],
|
||||||
|
]]));
|
||||||
|
};
|
||||||
|
|
||||||
|
$getKeysIdsForValidatorsDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface {
|
||||||
|
$expectedQuery = [
|
||||||
|
'filter_keys' => 'post.num_comments',
|
||||||
|
'filter_filenames' => 'validators.xliff',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertSame('GET', $method);
|
||||||
|
$this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/keys?'.http_build_query($expectedQuery), $url);
|
||||||
|
$this->assertSame($expectedQuery, $options['query']);
|
||||||
|
|
||||||
|
return new MockResponse(json_encode(['keys' => [
|
||||||
|
[
|
||||||
|
'key_name' => ['web' => 'post.num_comments'],
|
||||||
|
'key_id' => 92,
|
||||||
|
],
|
||||||
|
]]));
|
||||||
|
};
|
||||||
|
|
||||||
|
$deleteResponse = function (string $method, string $url, array $options = []): MockResponse {
|
||||||
|
$this->assertSame('DELETE', $method);
|
||||||
|
$this->assertSame(json_encode(['keys' => [29, 92]]), $options['body']);
|
||||||
|
|
||||||
|
return new MockResponse();
|
||||||
|
};
|
||||||
|
|
||||||
|
$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([
|
||||||
|
$getKeysIdsForMessagesDomainResponse,
|
||||||
|
$getKeysIdsForValidatorsDomainResponse,
|
||||||
|
$deleteResponse,
|
||||||
|
], 'https://api.lokalise.com/api2/projects/PROJECT_ID/'),
|
||||||
|
$this->getLoader(),
|
||||||
|
$this->getLogger(),
|
||||||
|
$this->getDefaultLocale(),
|
||||||
|
'api.lokalise.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 xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||||
|
<file original="" datatype="plaintext" xml:space="preserve" source-language="en" target-language="en">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="lokalise.com" tool-name="Lokalise"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="index.greetings" resname="index.greetings">
|
||||||
|
<source>index.greetings</source>
|
||||||
|
<target>Welcome, {firstname}!</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="index.hello" resname="index.hello">
|
||||||
|
<source>index.hello</source>
|
||||||
|
<target>Hello</target>
|
||||||
|
</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 xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||||
|
<file original="" datatype="plaintext" xml:space="preserve" source-language="en" target-language="fr">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="lokalise.com" tool-name="Lokalise"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="index.greetings" resname="index.greetings">
|
||||||
|
<source>index.greetings</source>
|
||||||
|
<target>Bienvenue, {firstname} !</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="index.hello" resname="index.hello">
|
||||||
|
<source>index.hello</source>
|
||||||
|
<target>Bonjour</target>
|
||||||
|
</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 xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||||
|
<file original="" datatype="plaintext" xml:space="preserve" source-language="en" target-language="en">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="lokalise.com" tool-name="Lokalise"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="index.greetings" resname="index.greetings">
|
||||||
|
<source>index.greetings</source>
|
||||||
|
<target>Welcome, {firstname}!</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="index.hello" resname="index.hello">
|
||||||
|
<source>index.hello</source>
|
||||||
|
<target>Hello</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
,
|
||||||
|
'validators' => <<<'XLIFF'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||||
|
<file original="" datatype="plaintext" xml:space="preserve" source-language="en" target-language="en">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="lokalise.com" tool-name="Lokalise"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="lastname.error" resname="lastname.error">
|
||||||
|
<source>lastname.error</source>
|
||||||
|
<target>Lastname must contains only letters.</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="firstname.error" resname="firstname.error">
|
||||||
|
<source>firstname.error</source>
|
||||||
|
<target>Firstname must contains only letters.</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
,
|
||||||
|
],
|
||||||
|
'fr' => [
|
||||||
|
'messages' => <<<'XLIFF'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||||
|
<file original="" datatype="plaintext" xml:space="preserve" source-language="en" target-language="fr">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="lokalise.com" tool-name="Lokalise"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="index.greetings" resname="index.greetings">
|
||||||
|
<source>index.greetings</source>
|
||||||
|
<target>Bienvenue, {firstname} !</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="index.hello" resname="index.hello">
|
||||||
|
<source>index.hello</source>
|
||||||
|
<target>Bonjour</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
,
|
||||||
|
'validators' => <<<'XLIFF'
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||||
|
<file original="" datatype="plaintext" xml:space="preserve" source-language="en" target-language="fr">
|
||||||
|
<header>
|
||||||
|
<tool tool-id="lokalise.com" tool-name="Lokalise"/>
|
||||||
|
</header>
|
||||||
|
<body>
|
||||||
|
<trans-unit id="lastname.error" resname="lastname.error">
|
||||||
|
<source>lastname.error</source>
|
||||||
|
<target>Le nom de famille ne peut contenir que des lettres.</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="firstname.error" resname="firstname.error">
|
||||||
|
<source>firstname.error</source>
|
||||||
|
<target>Le prénom ne peut contenir que des lettres.</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
XLIFF
|
||||||
|
,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
$expectedTranslatorBag,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "symfony/lokalise-translation-provider",
|
||||||
|
"type": "symfony-bridge",
|
||||||
|
"description": "Symfony Lokalise Translation Provider Bridge",
|
||||||
|
"keywords": ["lokalise", "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\\Lokalise\\": "" },
|
||||||
|
"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 Lokalise 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>
|
@ -25,6 +25,10 @@ class UnsupportedSchemeException extends LogicException
|
|||||||
'class' => Bridge\Loco\LocoProviderFactory::class,
|
'class' => Bridge\Loco\LocoProviderFactory::class,
|
||||||
'package' => 'symfony/loco-translation-provider',
|
'package' => 'symfony/loco-translation-provider',
|
||||||
],
|
],
|
||||||
|
'lokalise' => [
|
||||||
|
'class' => Bridge\Lokalise\LokaliseProviderFactory::class,
|
||||||
|
'package' => 'symfony/lokalise-translation-provider',
|
||||||
|
],
|
||||||
'poeditor' => [
|
'poeditor' => [
|
||||||
'class' => Bridge\PoEditor\PoEditorProviderFactory::class,
|
'class' => Bridge\PoEditor\PoEditorProviderFactory::class,
|
||||||
'package' => 'symfony/po-editor-translation-provider',
|
'package' => 'symfony/po-editor-translation-provider',
|
||||||
|
Reference in New Issue
Block a user