/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php
PHP | 357 lines | 257 code | 66 blank | 34 comment | 19 complexity | 2d4801e0e0e89571cd752b43d81debb3 MD5 | raw file
- <?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
- */
- final class LokaliseProvider implements ProviderInterface
- {
- private const LOKALISE_GET_KEYS_LIMIT = 5000;
- private HttpClientInterface $client;
- private LoaderInterface $loader;
- private LoggerInterface $logger;
- private string $defaultLocale;
- private string $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([], $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_recursive($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),
- 'export_empty_as' => 'skip',
- ],
- ]);
- $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;
- }
- $keys = $response->toArray(false)['keys'] ?? [];
- $createdKeys = array_reduce($keys, static 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): void
- {
- $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(), static 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;
- }, []),
- ];
- }
- }
- $response = $this->client->request('PUT', 'keys', [
- 'json' => ['keys' => $keysToUpdate],
- ]);
- 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, int $page = 1): array
- {
- $response = $this->client->request('GET', 'keys', [
- 'query' => [
- 'filter_keys' => implode(',', $keys),
- 'filter_filenames' => $this->getLokaliseFilenameFromDomain($domain),
- 'limit' => self::LOKALISE_GET_KEYS_LIMIT,
- 'page' => $page,
- ],
- ]);
- if (200 !== $response->getStatusCode()) {
- $this->logger->error(sprintf('Unable to get keys ids from Lokalise: "%s".', $response->getContent(false)));
- }
- $result = [];
- $keysFromResponse = $response->toArray(false)['keys'] ?? [];
- if (\count($keysFromResponse) > 0) {
- $result = array_reduce($keysFromResponse, static function ($carry, array $keyItem) {
- $carry[$keyItem['key_name']['web']] = $keyItem['key_id'];
- return $carry;
- }, []);
- }
- $paginationTotalCount = $response->getHeaders(false)['x-pagination-total-count'] ?? [];
- $keysTotalCount = (int) (reset($paginationTotalCount) ?? 0);
- if (0 === $keysTotalCount) {
- return $result;
- }
- $pages = ceil($keysTotalCount / self::LOKALISE_GET_KEYS_LIMIT);
- if ($page < $pages) {
- $result = array_merge($result, $this->getKeysIds($keys, $domain, ++$page));
- }
- return $result;
- }
- private function ensureAllLocalesAreCreated(TranslatorBagInterface $translatorBag): void
- {
- $providerLanguages = $this->getLanguages();
- $missingLanguages = array_reduce($translatorBag->getCatalogues(), static function ($carry, $catalogue) use ($providerLanguages) {
- if (!\in_array($catalogue->getLocale(), $providerLanguages, true)) {
- $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_column($responseContent['languages'], 'lang_iso');
- }
- return [];
- }
- private function createLanguages(array $languages): void
- {
- $response = $this->client->request('POST', 'languages', [
- 'json' => [
- 'languages' => array_map(static 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);
- }
- }