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 ;
36+ /** @var list<string> */
37+ private const FAILED_PROCESS_STATUS = ['cancelled ' , 'failed ' ];
38+ private const SUCESS_PROCESS_STATUS = 'finished ' ;
3439
3540 private HttpClientInterface $ client ;
3641 private LoaderInterface $ loader ;
@@ -165,6 +170,13 @@ private function exportFiles(array $locales, array $domains): array
165170 }
166171
167172 if (200 !== $ response ->getStatusCode ()) {
173+ if (($ errorCode = $ responseContent ['error ' ]['code ' ] ?? null ) && self ::PROJECT_TOO_BIG_STATUS_CODE === $ errorCode ) {
174+ if (!\extension_loaded ('zip ' )) {
175+ 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 );
176+ }
177+
178+ return $ this ->exportFilesAsync ($ locales , $ domains );
179+ }
168180 throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
169181 }
170182
@@ -176,6 +188,113 @@ private function exportFiles(array $locales, array $domains): array
176188 return array_combine ($ reformattedLanguages , $ responseContent ['files ' ]);
177189 }
178190
191+ /**
192+ * @see https://developers.lokalise.com/reference/download-files-async
193+ */
194+ private function exportFilesAsync (array $ locales , array $ domains ): array
195+ {
196+ $ response = $ this ->client ->request ('POST ' , 'files/async-download ' , [
197+ 'json ' => [
198+ 'format ' => 'symfony_xliff ' ,
199+ 'original_filenames ' => true ,
200+ 'filter_langs ' => array_values ($ locales ),
201+ 'filter_filenames ' => array_map ($ this ->getLokaliseFilenameFromDomain (...), $ domains ),
202+ 'export_empty_as ' => 'skip ' ,
203+ 'replace_breaks ' => false ,
204+ ],
205+ ]);
206+
207+ if (200 !== $ response ->getStatusCode ()) {
208+ throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
209+ }
210+
211+ $ processId = $ response ->toArray ()['process_id ' ];
212+ $ attempt = 0 ;
213+ while (true ) {
214+ $ response = $ this ->client ->request ('GET ' , \sprintf ('processes/%s ' , $ processId ));
215+ $ process = $ response ->toArray ()['process ' ];
216+ if (\in_array ($ process ['status ' ], self ::FAILED_PROCESS_STATUS , true )) {
217+ throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
218+ }
219+ if (self ::SUCESS_PROCESS_STATUS === $ process ['status ' ]) {
220+ $ downloadUrl = $ process ['details ' ]['download_url ' ];
221+ break ;
222+ }
223+ ++$ attempt ;
224+ usleep (500000 * $ attempt );
225+ }
226+
227+ $ response = $ this ->client ->request ('GET ' , $ downloadUrl , ['buffer ' => true ]);
228+ if (200 !== $ response ->getStatusCode ()) {
229+ throw new ProviderException (\sprintf ('Unable to download translations file from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
230+ }
231+ $ newfile = \sprintf ('%s%s%s.zip ' , sys_get_temp_dir (), \DIRECTORY_SEPARATOR , uniqid ());
232+ $ extractPath = \sprintf ('%s%s%s ' , sys_get_temp_dir (), \DIRECTORY_SEPARATOR , uniqid ());
233+ try {
234+ $ fileHandler = fopen ($ newfile , 'w ' );
235+ foreach ($ this ->client ->stream ($ response ) as $ chunk ) {
236+ fwrite ($ fileHandler , $ chunk ->getContent ());
237+ }
238+ fclose ($ fileHandler );
239+
240+ $ zip = new \ZipArchive ();
241+ if (!$ zip ->open ($ newfile , \ZipArchive::CREATE )) {
242+ throw new LogicException (\sprintf ('failed to open zip file "%s". ' , $ newfile ));
243+ }
244+
245+ try {
246+ if (!$ zip ->extractTo ($ extractPath )) {
247+ throw new LogicException (\sprintf ('failed to extract content from zip file "%s". ' , $ newfile ));
248+ }
249+ } finally {
250+ $ zip ->close ();
251+ }
252+
253+ $ fileContents = $ this ->getFileContents ($ extractPath );
254+
255+ // Lokalise returns languages with "-" separator, we need to reformat them to "_" separator.
256+ /** @var array<array-key, array-key> $reformattedLanguages */
257+ $ reformattedLanguages = array_map (static fn ($ language ) => str_replace ('- ' , '_ ' , $ language ), array_keys ($ fileContents ));
258+
259+ return array_combine ($ reformattedLanguages , $ fileContents );
260+ } finally {
261+ unlink ($ newfile );
262+ $ this ->removeDir ($ extractPath );
263+ }
264+ }
265+
266+ private function getFileContents (string $ dir , string $ baseDir = '' ): array
267+ {
268+ $ fileContents = [];
269+ foreach (scandir ($ dir ) as $ filename ) {
270+ if (\in_array ($ filename , ['. ' , '.. ' ])) {
271+ continue ;
272+ }
273+ $ path = \sprintf ('%s%s%s ' , $ dir , \DIRECTORY_SEPARATOR , $ filename );
274+ if (is_dir ($ path )) {
275+ $ fileContents = array_merge ($ fileContents , $ this ->getFileContents ($ path , $ filename ));
276+ continue ;
277+ }
278+ $ fileContents [$ baseDir ][$ filename ]['content ' ] = file_get_contents ($ path );
279+ }
280+
281+ return $ fileContents ;
282+ }
283+
284+ private function removeDir (string $ dir ): void
285+ {
286+ $ it = new \RecursiveDirectoryIterator ($ dir , \RecursiveDirectoryIterator::SKIP_DOTS );
287+ $ files = new \RecursiveIteratorIterator ($ it , \RecursiveIteratorIterator::CHILD_FIRST );
288+ foreach ($ files as $ file ) {
289+ if ($ file ->isDir ()) {
290+ rmdir ($ file ->getPathname ());
291+ } else {
292+ unlink ($ file ->getPathname ());
293+ }
294+ }
295+ rmdir ($ dir );
296+ }
297+
179298 private function createKeys (array $ keys , string $ domain ): array
180299 {
181300 $ keysToCreate = [];
0 commit comments