1212namespace Symfony \Component \Translation \Bridge \Lokalise ;
1313
1414use Psr \Log \LoggerInterface ;
15+ use Symfony \Component \Translation \Exception \LogicException ;
1516use Symfony \Component \Translation \Exception \ProviderException ;
1617use Symfony \Component \Translation \Loader \LoaderInterface ;
1718use Symfony \Component \Translation \MessageCatalogueInterface ;
3132final class LokaliseProvider implements ProviderInterface
3233{
3334 private const LOKALISE_GET_KEYS_LIMIT = 5000 ;
35+ private const PROJECT_TOO_BIG_STATUS_CODE = 413 ;
3436
3537 private HttpClientInterface $ client ;
3638 private LoaderInterface $ loader ;
@@ -165,6 +167,13 @@ private function exportFiles(array $locales, array $domains): array
165167 }
166168
167169 if (200 !== $ response ->getStatusCode ()) {
170+ if (($ errorCode = $ responseContent ['error ' ]['code ' ] ?? null ) && self ::PROJECT_TOO_BIG_STATUS_CODE === $ errorCode ) {
171+ if (!\extension_loaded ('zip ' )) {
172+ throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". There are too many translations. You need to enable the "zip" extension to use asynchronous export. ' , $ response ->getContent (false )), $ response );
173+ }
174+
175+ return $ this ->exportFilesAsync ($ locales , $ domains );
176+ }
168177 throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
169178 }
170179
@@ -176,6 +185,109 @@ private function exportFiles(array $locales, array $domains): array
176185 return array_combine ($ reformattedLanguages , $ responseContent ['files ' ]);
177186 }
178187
188+ /**
189+ * @see https://developers.lokalise.com/reference/download-files-async
190+ */
191+ private function exportFilesAsync (array $ locales , array $ domains ): array
192+ {
193+ $ response = $ this ->client ->request ('POST ' , 'files/async-download ' , [
194+ 'json ' => [
195+ 'format ' => 'symfony_xliff ' ,
196+ 'original_filenames ' => true ,
197+ 'filter_langs ' => array_values ($ locales ),
198+ 'filter_filenames ' => array_map ($ this ->getLokaliseFilenameFromDomain (...), $ domains ),
199+ 'export_empty_as ' => 'skip ' ,
200+ 'replace_breaks ' => false ,
201+ ],
202+ ]);
203+
204+ if (200 !== $ response ->getStatusCode ()) {
205+ throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
206+ }
207+
208+ $ processId = $ response ->toArray ()['process_id ' ];
209+ $ attempt = 0 ;
210+ while (true ) {
211+ $ response = $ this ->client ->request ('GET ' , \sprintf ('processes/%s ' , $ processId ));
212+ $ process = $ response ->toArray ()['process ' ];
213+ if ('failed ' === $ process ['status ' ]) {
214+ throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
215+ }
216+ if ('finished ' === $ process ['status ' ]) {
217+ $ downloadUrl = $ process ['details ' ]['download_url ' ];
218+ break ;
219+ }
220+ ++$ attempt ;
221+ usleep (500000 * $ attempt );
222+ }
223+
224+ $ newfile = \sprintf ('%s%s%s.zip ' , sys_get_temp_dir (), \DIRECTORY_SEPARATOR , uniqid ());
225+ $ extractPath = \sprintf ('%s%s%s ' , sys_get_temp_dir (), \DIRECTORY_SEPARATOR , uniqid ());
226+ if (!copy ($ downloadUrl , $ newfile )) {
227+ throw new LogicException (\sprintf ('failed to copy "%s". ' , $ downloadUrl ));
228+ }
229+
230+ try {
231+ $ zip = new \ZipArchive ();
232+ if (!$ zip ->open ($ newfile , \ZipArchive::CREATE )) {
233+ throw new LogicException (\sprintf ('failed to open zip file "%s". ' , $ newfile ));
234+ }
235+
236+ try {
237+ if (!$ zip ->extractTo ($ extractPath )) {
238+ throw new LogicException (\sprintf ('failed to extract content from zip file "%s". ' , $ newfile ));
239+ }
240+ } finally {
241+ $ zip ->close ();
242+ }
243+
244+ $ fileContents = $ this ->getFileContents ($ extractPath );
245+
246+ // Lokalise returns languages with "-" separator, we need to reformat them to "_" separator.
247+ /** @var array<array-key, array-key> $reformattedLanguages */
248+ $ reformattedLanguages = array_map (function ($ language ) {
249+ return str_replace ('- ' , '_ ' , $ language );
250+ }, array_keys ($ fileContents ));
251+
252+ return array_combine ($ reformattedLanguages , $ fileContents );
253+ } finally {
254+ unlink ($ newfile );
255+ $ this ->removeDir ($ extractPath );
256+ }
257+ }
258+
259+ private function getFileContents (string $ dir , string $ baseDir = '' ): array
260+ {
261+ $ fileContents = [];
262+ foreach (scandir ($ dir ) as $ filename ) {
263+ if (\in_array ($ filename , ['. ' , '.. ' ])) {
264+ continue ;
265+ }
266+ $ path = \sprintf ('%s%s%s ' , $ dir , \DIRECTORY_SEPARATOR , $ filename );
267+ if (is_dir ($ path )) {
268+ $ fileContents = array_merge ($ fileContents , $ this ->getFileContents ($ path , $ filename ));
269+ continue ;
270+ }
271+ $ fileContents [$ baseDir ][$ filename ]['content ' ] = file_get_contents ($ path );
272+ }
273+
274+ return $ fileContents ;
275+ }
276+
277+ private function removeDir (string $ dir ): void
278+ {
279+ $ it = new \RecursiveDirectoryIterator ($ dir , \RecursiveDirectoryIterator::SKIP_DOTS );
280+ $ files = new \RecursiveIteratorIterator ($ it , \RecursiveIteratorIterator::CHILD_FIRST );
281+ foreach ($ files as $ file ) {
282+ if ($ file ->isDir ()) {
283+ rmdir ($ file ->getPathname ());
284+ } else {
285+ unlink ($ file ->getPathname ());
286+ }
287+ }
288+ rmdir ($ dir );
289+ }
290+
179291 private function createKeys (array $ keys , string $ domain ): array
180292 {
181293 $ keysToCreate = [];
0 commit comments