Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 749cca8

Browse filesBrowse files
committed
Added Lokalise Provider
1 parent dc9648a commit 749cca8
Copy full SHA for 749cca8

File tree

13 files changed

+453
-1
lines changed
Filter options

13 files changed

+453
-1
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+3-1Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
170170
use Symfony\Component\String\LazyString;
171171
use Symfony\Component\String\Slugger\SluggerInterface;
172172
use Symfony\Component\Translation\Bridge\Loco\LocoProviderFactory;
173+
use Symfony\Component\Translation\Bridge\Lokalise\LokaliseProviderFactory;
173174
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
174175
use Symfony\Component\Translation\PseudoLocalizationTranslator;
175176
use Symfony\Component\Translation\Translator;
@@ -1355,14 +1356,15 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder
13551356

13561357
$classToServices = [
13571358
LocoProviderFactory::class => 'translation.provider_factory.loco',
1359+
LokaliseProviderFactory::class => 'translation.provider_factory.lokalise',
13581360
];
13591361

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

13621364
foreach ($classToServices as $class => $service) {
13631365
$package = sprintf('symfony/%s-translation-provider', substr($service, \strlen('translation.provider_factory.')));
13641366

1365-
if (!$container->hasDefinition('http_client') || !ContainerBuilder::willBeAvailable($package, $class, $parentPackages)) {
1367+
if (!$container->hasDefinition('http_client') || !ContainerBuilder::willBeAvailable(sprintf('symfony/%s-translation', $package), $class, $parentPackages)) {
13661368
$container->removeDefinition($service);
13671369
}
13681370
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_providers.php
+10Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

1414
use Symfony\Component\Translation\Bridge\Loco\LocoProviderFactory;
15+
use Symfony\Component\Translation\Bridge\Lokalise\LokaliseProviderFactory;
1516
use Symfony\Component\Translation\Provider\NullProviderFactory;
1617
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
1718
use Symfony\Component\Translation\Provider\TranslationProviderCollectionFactory;
@@ -41,5 +42,14 @@
4142
service('translation.loader.xliff'),
4243
])
4344
->tag('translation.provider_factory')
45+
46+
->set('translation.provider_factory.loco', LokaliseProviderFactory::class)
47+
->args([
48+
service('http_client'),
49+
service('logger'),
50+
param('kernel.default_locale'),
51+
service('translation.loader.xliff'),
52+
])
53+
->tag('translation.provider_factory')
4454
;
4555
};
+4Lines 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
+3Lines 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
+7Lines 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
+19Lines 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.
+206Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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\Lokalise;
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+
* @experimental in 5.3
26+
*
27+
* In Lokalise:
28+
* * Filenames refers to Symfony's translation domains;
29+
* * Keys refers to Symfony's translation keys;
30+
* * Translations refers to Symfony's translated messages
31+
*/
32+
final class LokaliseProvider implements ProviderInterface
33+
{
34+
private $projectId;
35+
private $client;
36+
private $loader;
37+
private $logger;
38+
private $defaultLocale;
39+
private $endpoint;
40+
41+
public function __construct(string $projectId, HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, string $defaultLocale, string $endpoint)
42+
{
43+
$this->projectId = $projectId;
44+
$this->client = $client;
45+
$this->loader = $loader;
46+
$this->logger = $logger;
47+
$this->defaultLocale = $defaultLocale;
48+
$this->endpoint = $endpoint;
49+
}
50+
51+
public function __toString(): string
52+
{
53+
return sprintf('%s://%s', LokaliseProviderFactory::SCHEME, $this->endpoint);
54+
}
55+
56+
/**
57+
* {@inheritdoc}
58+
*
59+
* Lokalise API recommends sending payload in chunks of up to 500 keys per request.
60+
*
61+
* @see https://app.lokalise.com/api2docs/curl/#transition-create-keys-post
62+
*/
63+
public function write(TranslatorBagInterface $translatorBag): void
64+
{
65+
$keys = [];
66+
$defaultCatalogue = $translatorBag->getCatalogue($this->defaultLocale);
67+
68+
if (!$defaultCatalogue) {
69+
$defaultCatalogue = $translatorBag->getCatalogues()[0];
70+
}
71+
72+
foreach ($defaultCatalogue->getDomains() as $domain) {
73+
foreach ($defaultCatalogue->all($domain) as $key => $message) {
74+
$keys[] = [
75+
'key_name' => $key,
76+
'platforms' => ['web'],
77+
'filenames' => [
78+
'web' => $this->getLokaliseFilenameFromDomain($domain),
79+
// There is a bug in Lokalise with "Per platform key names" option enabled,
80+
// we need to provide a filename for all platforms.
81+
'ios' => null,
82+
'android' => null,
83+
'other' => null,
84+
],
85+
'translations' => array_map(function ($catalogue) use ($key, $domain) {
86+
return [
87+
'language_iso' => $catalogue->getLocale(),
88+
'translation' => $catalogue->get($key, $domain),
89+
];
90+
}, $translatorBag->getCatalogues()),
91+
];
92+
}
93+
}
94+
95+
$chunks = array_chunk($keys, 500);
96+
97+
foreach ($chunks as $chunk) {
98+
$response = $this->client->request('POST', sprintf('/projects/%s/keys', $this->projectId), [
99+
'json' => ['keys' => $chunk],
100+
]);
101+
102+
if (200 !== $response->getStatusCode()) {
103+
throw new ProviderException(sprintf('Unable to add keys and translations to Lokalise: "%s".', $response->getContent(false)), $response);
104+
}
105+
}
106+
}
107+
108+
public function read(array $domains, array $locales): TranslatorBag
109+
{
110+
$translatorBag = new TranslatorBag();
111+
$translations = $this->exportFiles($locales, $domains);
112+
113+
foreach ($translations as $locale => $files) {
114+
foreach ($files as $filename => $content) {
115+
$translatorBag->addCatalogue($this->loader->load($content['content'], $locale, str_replace('.xliff', '', $filename)));
116+
}
117+
}
118+
119+
return $translatorBag;
120+
}
121+
122+
public function delete(TranslatorBagInterface $translatorBag): void
123+
{
124+
$catalogue = $translatorBag->getCatalogue($this->defaultLocale);
125+
126+
if (!$catalogue) {
127+
$catalogue = $translatorBag->getCatalogues()[0];
128+
}
129+
130+
$keysIds = [];
131+
foreach ($catalogue->all() as $messagesByDomains) {
132+
foreach ($messagesByDomains as $domain => $messages) {
133+
$keysToDelete = [];
134+
foreach ($messages as $message) {
135+
$keysToDelete[] = $message;
136+
}
137+
$keysIds += $this->getKeysIds($keysToDelete, $domain);
138+
}
139+
}
140+
141+
$response = $this->client->request('DELETE', sprintf('/projects/%s/keys', $this->projectId), [
142+
'json' => ['keys' => $keysIds],
143+
]);
144+
145+
if (200 !== $response->getStatusCode()) {
146+
throw new ProviderException(sprintf('Unable to delete keys from Lokalise: "%s".', $response->getContent(false)), $response);
147+
}
148+
}
149+
150+
/**
151+
* @see https://app.lokalise.com/api2docs/curl/#transition-download-files-post
152+
*/
153+
private function exportFiles(array $locales, array $domains): array
154+
{
155+
$response = $this->client->request('POST', sprintf('/projects/%s/files/export', $this->projectId), [
156+
'json' => [
157+
'format' => 'symfony_xliff',
158+
'original_filenames' => true,
159+
'directory_prefix' => '%LANG_ISO%',
160+
'filter_langs' => array_values($locales),
161+
'filter_filenames' => array_map([$this, 'getLokaliseFilenameFromDomain'], $domains),
162+
],
163+
]);
164+
165+
$responseContent = $response->toArray(false);
166+
167+
if (406 === $response->getStatusCode()
168+
&& 'No keys found with specified filenames.' === $responseContent['error']['message']
169+
) {
170+
return [];
171+
}
172+
173+
if (200 !== $response->getStatusCode()) {
174+
throw new ProviderException(sprintf('Unable to export translations from Lokalise: "%s".', $response->getContent(false)), $response);
175+
}
176+
177+
return $responseContent['files'];
178+
}
179+
180+
private function getKeysIds(array $keys, string $domain): array
181+
{
182+
$response = $this->client->request('GET', sprintf('/projects/%s/keys', $this->projectId), [
183+
'query' => [
184+
'filter_keys' => $keys,
185+
'filter_filenames' => $this->getLokaliseFilenameFromDomain($domain),
186+
],
187+
]);
188+
189+
$responseContent = $response->toArray(false);
190+
191+
if (200 !== $response->getStatusCode()) {
192+
throw new ProviderException(sprintf('Unable to get keys ids from Lokalise: "%s".', $response->getContent(false)), $response);
193+
}
194+
195+
return array_reduce($responseContent['keys'], function ($keysIds, array $keyItem) {
196+
$keysIds[] = $keyItem['key_id'];
197+
198+
return $keysIds;
199+
}, []);
200+
}
201+
202+
private function getLokaliseFilenameFromDomain(string $domain): string
203+
{
204+
return sprintf('%s.xliff', $domain);
205+
}
206+
}
+69Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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\Lokalise;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\Translation\Exception\UnsupportedSchemeException;
16+
use Symfony\Component\Translation\Loader\LoaderInterface;
17+
use Symfony\Component\Translation\Provider\AbstractProviderFactory;
18+
use Symfony\Component\Translation\Provider\Dsn;
19+
use Symfony\Component\Translation\Provider\ProviderInterface;
20+
use Symfony\Contracts\HttpClient\HttpClientInterface;
21+
22+
/**
23+
* @author Mathieu Santostefano <msantostefano@protonmail.com>
24+
*
25+
* @experimental in 5.3
26+
*/
27+
final class LokaliseProviderFactory extends AbstractProviderFactory
28+
{
29+
public const SCHEME = 'lokalise';
30+
private const HOST = 'api.lokalise.com/api2/';
31+
32+
private $client;
33+
private $logger;
34+
private $defaultLocale;
35+
private $loader;
36+
37+
public function __construct(HttpClientInterface $client, LoggerInterface $logger, string $defaultLocale, LoaderInterface $loader)
38+
{
39+
$this->client = $client;
40+
$this->logger = $logger;
41+
$this->defaultLocale = $defaultLocale;
42+
$this->loader = $loader;
43+
}
44+
45+
/**
46+
* @return LokaliseProvider
47+
*/
48+
public function create(Dsn $dsn): ProviderInterface
49+
{
50+
if (self::SCHEME === $dsn->getScheme()) {
51+
$endpoint = sprintf('%s%s', 'default' === $dsn->getHost() ? self::HOST : $dsn->getHost(), $dsn->getPort() ? ':'.$dsn->getPort() : '');
52+
$client = $this->client->withOptions([
53+
'base_uri' => 'https://'.$endpoint,
54+
'headers' => [
55+
'X-Api-Token' => $this->getPassword($dsn),
56+
],
57+
]);
58+
59+
return new LokaliseProvider($this->getUser($dsn), $client, $this->loader, $this->logger, $this->defaultLocale, $endpoint);
60+
}
61+
62+
throw new UnsupportedSchemeException($dsn, self::SCHEME, $this->getSupportedSchemes());
63+
}
64+
65+
protected function getSupportedSchemes(): array
66+
{
67+
return [self::SCHEME];
68+
}
69+
}
+28Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Lokalise Translation Provider
2+
=============================
3+
4+
Provides Lokalise integration for Symfony Translation.
5+
6+
DSN example
7+
-----------
8+
9+
```
10+
// .env file
11+
LOKALISE_DSN=lokalise://PROJECT_ID:API_KEY@default
12+
```
13+
14+
where:
15+
- `PROJECT_ID` is your Lokalise Project ID
16+
- `API_KEY` is your Lokalise API key
17+
18+
Go to the Project Settings in Lokalise to find the Project ID.
19+
20+
[Generate an API key on Lokalise](https://app.lokalise.com/api2docs/curl/#resource-authentication)
21+
22+
Resources
23+
---------
24+
25+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
26+
* [Report issues](https://github.com/symfony/symfony/issues) and
27+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
28+
in the [main Symfony repository](https://github.com/symfony/symfony)

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.