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,9 @@ 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 && \extension_loaded ('zip ' )) {
171+ return $ this ->exportFilesAsync ($ locales , $ domains );
172+ }
168173 throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
169174 }
170175
@@ -176,6 +181,109 @@ private function exportFiles(array $locales, array $domains): array
176181 return array_combine ($ reformattedLanguages , $ responseContent ['files ' ]);
177182 }
178183
184+ /**
185+ * @see https://developers.lokalise.com/reference/download-files-async
186+ */
187+ private function exportFilesAsync (array $ locales , array $ domains ): array
188+ {
189+ $ response = $ this ->client ->request ('POST ' , 'files/async-download ' , [
190+ 'json ' => [
191+ 'format ' => 'symfony_xliff ' ,
192+ 'original_filenames ' => true ,
193+ 'filter_langs ' => array_values ($ locales ),
194+ 'filter_filenames ' => array_map ($ this ->getLokaliseFilenameFromDomain (...), $ domains ),
195+ 'export_empty_as ' => 'skip ' ,
196+ 'replace_breaks ' => false ,
197+ ],
198+ ]);
199+
200+ if (200 !== $ response ->getStatusCode ()) {
201+ throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
202+ }
203+
204+ $ processId = $ response ->toArray ()['process_id ' ];
205+ $ attempt = 0 ;
206+ while (true ) {
207+ $ response = $ this ->client ->request ('GET ' , \sprintf ('processes/%s ' , $ processId ));
208+ $ process = $ response ->toArray ()['process ' ];
209+ if ('failed ' === $ process ['status ' ]) {
210+ throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
211+ }
212+ if ('finished ' === $ process ['status ' ]) {
213+ $ downloadUrl = $ process ['details ' ]['download_url ' ];
214+ break ;
215+ }
216+ ++$ attempt ;
217+ usleep (500000 * $ attempt );
218+ }
219+
220+ $ newfile = \sprintf ('%s%s%s.zip ' , sys_get_temp_dir (), \DIRECTORY_SEPARATOR , uniqid ());
221+ $ extractPath = \sprintf ('%s%s%s ' , sys_get_temp_dir (), \DIRECTORY_SEPARATOR , uniqid ());
222+ if (!copy ($ downloadUrl , $ newfile )) {
223+ throw new LogicException (\sprintf ('failed to copy "%s". ' , $ downloadUrl ));
224+ }
225+
226+ try {
227+ $ zip = new \ZipArchive ();
228+ if (!$ zip ->open ($ newfile , \ZipArchive::CREATE )) {
229+ throw new LogicException (\sprintf ('failed to open zip file "%s". ' , $ newfile ));
230+ }
231+
232+ try {
233+ if (!$ zip ->extractTo ($ extractPath )) {
234+ throw new LogicException (\sprintf ('failed to extract content from zip file "%s". ' , $ newfile ));
235+ }
236+ } finally {
237+ $ zip ->close ();
238+ }
239+
240+ $ fileContents = $ this ->getFileContents ($ extractPath );
241+
242+ // Lokalise returns languages with "-" separator, we need to reformat them to "_" separator.
243+ /** @var array<array-key, array-key> $reformattedLanguages */
244+ $ reformattedLanguages = array_map (function ($ language ) {
245+ return str_replace ('- ' , '_ ' , $ language );
246+ }, array_keys ($ fileContents ));
247+
248+ return array_combine ($ reformattedLanguages , $ fileContents );
249+ } finally {
250+ unlink ($ newfile );
251+ $ this ->removeDir ($ extractPath );
252+ }
253+ }
254+
255+ private function getFileContents (string $ dir , string $ baseDir = '' ): array
256+ {
257+ $ fileContents = [];
258+ foreach (scandir ($ dir ) as $ filename ) {
259+ if (\in_array ($ filename , ['. ' , '.. ' ])) {
260+ continue ;
261+ }
262+ $ path = \sprintf ('%s%s%s ' , $ dir , \DIRECTORY_SEPARATOR , $ filename );
263+ if (is_dir ($ path )) {
264+ $ fileContents = array_merge ($ fileContents , $ this ->getFileContents ($ path , $ filename ));
265+ continue ;
266+ }
267+ $ fileContents [$ baseDir ][$ filename ]['content ' ] = file_get_contents ($ path );
268+ }
269+
270+ return $ fileContents ;
271+ }
272+
273+ private function removeDir (string $ dir ): void
274+ {
275+ $ it = new \RecursiveDirectoryIterator ($ dir , \RecursiveDirectoryIterator::SKIP_DOTS );
276+ $ files = new \RecursiveIteratorIterator ($ it , \RecursiveIteratorIterator::CHILD_FIRST );
277+ foreach ($ files as $ file ) {
278+ if ($ file ->isDir ()) {
279+ rmdir ($ file ->getPathname ());
280+ } else {
281+ unlink ($ file ->getPathname ());
282+ }
283+ }
284+ rmdir ($ dir );
285+ }
286+
179287 private function createKeys (array $ keys , string $ domain ): array
180288 {
181289 $ keysToCreate = [];
0 commit comments