8000 Added PoEditor Provider · symfony/symfony@240ac22 · GitHub
[go: up one dir, main page]

Skip to content

Commit 240ac22

Browse files
committed
Added PoEditor Provider
1 parent c151f76 commit 240ac22

File tree

18 files changed

+1022
-10
lines changed

18 files changed

+1022
-10
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@
171171
use Symfony\Component\String\Slugger\SluggerInterface;
172172
use Symfony\Component\Translation\Bridge\Crowdin\CrowdinProviderFactory;
173173
use Symfony\Component\Translation\Bridge\Loco\LocoProviderFactory;
174+
use Symfony\Component\Translation\Bridge\PoEditor\PoEditorProviderFactory;
174175
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
175176
use Symfony\Component\Translation\PseudoLocalizationTranslator;
176177
use Symfony\Component\Translation\Translator;
@@ -1344,14 +1345,17 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder
13441345
$classToServices = [
13451346
CrowdinProviderFactory::class => 'translation.provider_factory.crowdin',
13461347
LocoProviderFactory::class => 'translation.provider_factory.loco',
1348+
PoEditorProviderFactory::class => 'translation.provider_factory.poeditor',
13471349
];
13481350

13491351
$parentPackages = ['symfony/framework-bundle', 'symfony/translation', 'symfony/http-client'];
13501352

13511353
foreach ($classToServices as $class => $service) {
1352-
$package = sprintf('symfony/%s-translation-provider', substr($service, \strlen('translation.provider_factory.')));
1354+
switch ($package = substr($service, \strlen('translation.provider_factory.'))) {
1355+
case 'poeditor': $package = 'po-editor'; break;
1356+
}
13531357

1354-
if (!$container->hasDefinition('http_client') || !ContainerBuilder::willBeAvailable($package, $class, $parentPackages)) {
1358+
if (!$container->hasDefinition('http_client') || !ContainerBuilder::willBeAvailable(sprintf('symfony/%s-translation-provider', $package), $class, $parentPackages)) {
13551359
$container->removeDefinition($service);
13561360
}
13571361
}

src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_providers.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Translation\Bridge\Crowdin\CrowdinProviderFactory;
1515
use Symfony\Component\Translation\Bridge\Loco\LocoProviderFactory;
16+
use Symfony\Component\Translation\Bridge\PoEditor\PoEditorProviderFactory;
1617
use Symfony\Component\Translation\Provider\NullProviderFactory;
1718
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
1819
use Symfony\Component\Translation\Provider\TranslationProviderCollectionFactory;
@@ -52,5 +53,14 @@
5253
service('translation.loader.xliff'),
5354
])
5455
->tag('translation.provider_factory')
56+
57+
->set('translation.provider_factory.poeditor', PoEditorProviderFactory::class)
58+
->args([
59+
service('http_client'),
60+
service('logger'),
61+
param('kernel.default_locale'),
62+
service('translation.loader.xliff'),
63+
])
64+
->tag('translation.provider_factory')
5565
;
5666
};

src/Symfony/Component/Translation/Bridge/Crowdin/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ where:
2323
Resources
2424
---------
2525

26-
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
27-
* [Report issues](https://github.com/symfony/symfony/issues) and
28-
[send Pull Requests](https://github.com/symfony/symfony/pulls)
29-
in the [main Symfony repository](https://github.com/symfony/symfony)
26+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
27+
* [Report issues](https://github.com/symfony/symfony/issues) and
28+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
29+
n the [main Symfony repository](https://github.com/symfony/symfony)

src/Symfony/Component/Translation/Bridge/Loco/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ where:
1919
Resources
2020
---------
2121

22-
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
23-
* [Report issues](https://github.com/symfony/symfony/issues) and
24-
[send Pull Requests](https://github.com/symfony/symfony/pulls)
25-
in the [main Symfony repository](https://github.com/symfony/symfony)
22+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
23+
* [Report issues](https://github.com/symfony/symfony/issues) and
24+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
25+
in the [main Symfony repository](https://github.com/symfony/symfony)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.gitattributes export-ignore
4+
/.gitignore export-ignore
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
5.3
5+
---
6+
7+
* Create the bridge
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2021 Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Translation\Bridge\PoEditor;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\Translation\Exception\ProviderException;
16+
use Symfony\Component\Translation\Loader\LoaderInterface;
17+
use Symfony\Component\Translation\Provider\ProviderInterface;
18+
use Symfony\Component\Translation\TranslatorBag;
19+
use Symfony\Component\Translation\TranslatorBagInterface;
20+
use Symfony\Contracts\HttpClient\HttpClientInterface;
21+
22+
/**
23+
* @author Mathieu Santostefano <msantostefano@protonmail.com>
24+
*
25+
* In PoEditor:
26+
* * Terms refer to Symfony's translation keys;
27+
* * Translations refer to Symfony's translated messages;
28+
* * Context fields refer to Symfony's translation domains
29+
*
30+
* PoEditor's API always returns 200 status code, even in case of failure.
31+
*
32+
* @experimental in 5.3
33+
*/
34+
final class PoEditorProvider implements ProviderInterface
35+
{
36+
private $apiKey;
37+
private $projectId;
38+
private $client;
39+
private $loader;
40+
private $logger;
41+
private $defaultLocale;
42+
private $endpoint;
43+
44+
public function __construct(string $apiKey, string $projectId, HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, string $defaultLocale, string $endpoint)
45+
{
46+
$this->apiKey = $apiKey;
47+
$this->projectId = $projectId;
48+
$this->client = $client;
49+
$this->loader = $loader;
50+
$this->logger = $logger;
51+
$this->defaultLocale = $defaultLocale;
52+
$this->endpoint = $endpoint;
53+
}
54+
55+
public function __toString(): string
56+
{
57+
return sprintf('poeditor://%s', $this->endpoint);
58+
}
59+
60+
public function write(TranslatorBagInterface $translatorBag): void
61+
{
62+
$defaultCatalogue = $translatorBag->getCatalogue($this->defaultLocale);
63+
64+
if (!$defaultCatalogue) {
65+
$defaultCatalogue = $translatorBag->getCatalogues()[0];
66+
}
67+
68+
$terms = $translationsToAdd = [];
69+
foreach ($defaultCatalogue->all() as $domain => $messages) {
70+
foreach ($messages as $id => $message) {
71+
$terms[] = [
72+
'term' => $id,
73+
'reference' => 10000 ; $id,
74+
// tags field is mandatory to export all translations in read method.
75+
'tags' => [$domain],
76+
'context' => $domain,
77+
];
78+
}
79+
}
80+
$this->addTerms($terms);
81+
82+
foreach ($translatorBag->getCatalogues() as $catalogue) {
83+
$locale = $catalogue->getLocale();
84+
foreach ($catalogue->all() as $domain => $messages) {
85+
foreach ($messages as $id => $message) {
86+
$translationsToAdd[$locale][] = [
87+
'term' => $id,
88+
'context' => $domain,
89+
'translation' => [
90+
'content' => $message,
91+
],
92+
];
93+
}
94+
}
95+
}
96+
97+
$this->addTranslations($translationsToAdd);
98+
}
99+
100+
public function read(array $domains, array $locales): TranslatorBag
101+
{
102+
$translatorBag = new TranslatorBag();
103+
$exportResponses = $downloadResponses = [];
104+
105+
foreach ($locales as $locale) {
106+
foreach ($domains as $domain) {
107+
$exportResponses[] = [
108+
'response' => $this->client->request('POST', 'projects/export', [
109+
'body' => [
110+
'api_token' => $this->apiKey,
111+
'id' => $this->projectId,
112+
'language' => $locale,
113+
'type' => 'xlf',
114+
'filters' => json_encode(['translated']),
115+
'tags' => json_encode([$domain]),
116+
],
117+
]),
118+
'locale' => $locale,
119+
'domain' => $domain,
120+
];
121+
}
122+
}
123+
124+
foreach ($exportResponses as $exportResponse) {
125+
$response = $exportResponse['response'];
126+
$responseContent = $response->toArray(false);
127+
128+
if (200 !== $response->getStatusCode() || '200' !== (string) $responseContent['response']['code']) {
129+
$this->logger->error('Unable to read the PoEditor response: '.$response->getContent(false));
130+
continue;
131+
}
132+
133+
$fileUrl = $responseContent['result']['url'];
134+
$downloadResponses[] = [
135+
'response' => $this->client->request('GET', $fileUrl),
136+
'locale' => $exportResponse['locale'],
137+
'domain' => $exportResponse['domain'],
138+
'fileUrl' => $fileUrl,
139+
];
140+
}
141+
142+
foreach ($downloadResponses as $downloadResponse) {
143+
$response = $downloadResponse['response'];
144+
$locale = $downloadResponse['locale'];
145+
$domain = $downloadResponse['domain'];
146+
$fileUrl = $downloadResponse['fileUrl'];
147+
$responseContent = $response->getContent(false);
148+
149+
if (200 !== $response->getStatusCode()) {
150+
$this->logger->error('Unable to download the PoEditor exported file: '.$responseContent);
151+
continue;
152+
}
153+
154+
if (!$responseContent) {
155+
$this->logger->error(sprintf('The exported file "%s" from PoEditor is empty.', $fileUrl));
156+
continue;
157+
}
158+
159+
$translatorBag->addCatalogue($ 10000 this->loader->load($responseContent, $locale, $domain));
160+
}
161+
162+
return $translatorBag;
163+
}
164+
165+
public function delete(TranslatorBagInterface $translatorBag): void
166+
{
167+
$deletedIds = $termsToDelete = [];
168+
169+
foreach ($translatorBag->getCatalogues() as $catalogue) {
170+
foreach ($catalogue->all() as $domain => $messages) {
171+
foreach ($messages as $id => $message) {
172+
if (\array_key_exists($domain, $deletedIds) && \in_array($id, $deletedIds[$domain], true)) {
173+
continue;
174+
}
175+
176+
$deletedIds[$domain][] = $id;
177+
$termsToDelete[] = [
178+
'term' => $id,
179+
'context' => $domain,
180+
];
181+
}
182+
}
183+
}
184+
185+
$this->deleteTerms($termsToDelete);
186+
}
187+
188+
private function addTerms(array $terms): void
189+
{
190+
$response = $this->client->request('POST', 'terms/add', [
191+
'body' => [
192+
'api_token' => $this->apiKey,
193+
'id' => $this->projectId,
194+
'data' => json_encode($terms),
195+
],
196+
]);
197+
198+
if (200 !== $response->getStatusCode() || '200' !== (string) $response->toArray(false)['response']['code']) {
199+
throw new ProviderException(sprintf('Unable to add new translation keys to PoEditor: (status code: "%s") "%s".', $response->getStatusCode(), $response->getContent(false)), $response);
200+
}
201+
}
202+
203+
private function addTranslations(array $translationsPerLocale): void
204+
{
205+
$responses = [];
206+
207+
foreach ($translationsPerLocale as $locale => $translations) {
208+
$responses = $this->client->request('POST', 'translations/add', [
209+
'body' => [
210+
'api_token' => $this->apiKey,
211+
'id' => $this->projectId,
212+
'language' => $locale,
213+
'data' => json_encode($translations),
214+
],
215+
]);
216+
}
217+
218+
foreach ($responses as $response) {
219+
if (200 !== $response->getStatusCode() || '200' !== (string) $response->toArray(false)['response']['code']) {
220+
$this->logger->error(sprintf('Unable to add translation messages to PoEditor: "%s".', $response->getContent(false)));
221+
}
222+
}
223+
}
224+
225+
private function deleteTerms(array $ids): void
226+
{
227+
$response = $this->client->request('POST', 'terms/delete', [
228+
'body' => [
229+
'api_token' => $this->apiKey,
230+
'id' => $this->projectId,
231+
'data' => json_encode($ids),
232+
],
233+
]);
234+
235+
if (200 !== $response->getStatusCode() || '200' !== (string) $response->toArray(false)['response']['code']) {
236+
throw new ProviderException(sprintf('Unable to delete translation keys on PoEditor: "%s".', $response->getContent(false)), $response);
237+
}
238+
}
239+
}

0 commit comments

Comments
 (0)
0