1313
1414use Psr \Log \LoggerInterface ;
1515use Symfony \Component \Translation \Exception \ProviderException ;
16+ use Symfony \Component \Translation \Exception \RuntimeException ;
1617use Symfony \Component \Translation \Loader \LoaderInterface ;
1718use Symfony \Component \Translation \MessageCatalogueInterface ;
1819use Symfony \Component \Translation \Provider \ProviderInterface ;
3132final class LokaliseProvider implements ProviderInterface
3233{
3334 private const LOKALISE_GET_KEYS_LIMIT = 5000 ;
35+ private const PROJECT_TOO_BIG_STATUS_CODE = 413 ;
36+ private const FAILED_PROCESS_STATUS = ['cancelled ' , 'failed ' ];
37+ private const SUCESS_PROCESS_STATUS = 'finished ' ;
3438
3539 private HttpClientInterface $ client ;
3640 private LoaderInterface $ loader ;
@@ -165,7 +169,14 @@ private function exportFiles(array $locales, array $domains): array
165169 }
166170
167171 if (200 !== $ response ->getStatusCode ()) {
168- throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
172+ if (self ::PROJECT_TOO_BIG_STATUS_CODE !== ($ responseContent ['error ' ]['code ' ] ?? null )) {
173+ throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
174+ }
175+ if (!\extension_loaded ('zip ' )) {
176+ throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". Make sure that the "zip" extension is enabled. ' , $ response ->getContent (false )), $ response );
177+ }
178+
179+ return $ this ->exportFilesAsync ($ locales , $ domains );
169180 }
170181
171182 // Lokalise returns languages with "-" separator, we need to reformat them to "_" separator.
@@ -176,6 +187,115 @@ private function exportFiles(array $locales, array $domains): array
176187 return array_combine ($ reformattedLanguages , $ responseContent ['files ' ]);
177188 }
178189
190+ /**
191+ * @see https://developers.lokalise.com/reference/download-files-async
192+ */
193+ private function exportFilesAsync (array $ locales , array $ domains ): array
194+ {
195+ $ response = $ this ->client ->request ('POST ' , 'files/async-download ' , [
196+ 'json ' => [
197+ 'format ' => 'symfony_xliff ' ,
198+ 'original_filenames ' => true ,
199+ 'filter_langs ' => array_values ($ locales ),
200+ 'filter_filenames ' => array_map ($ this ->getLokaliseFilenameFromDomain (...), $ domains ),
201+ 'export_empty_as ' => 'skip ' ,
202+ 'replace_breaks ' => false ,
203+ ],
204+ ]);
205+
206+ if (200 !== $ response ->getStatusCode ()) {
207+ throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
208+ }
209+
210+ $ processId = $ response ->toArray ()['process_id ' ];
211+ while (true ) {
212+ $ response = $ this ->client ->request ('GET ' , \sprintf ('processes/%s ' , $ processId ));
213+ $ process = $ response ->toArray ()['process ' ];
214+ if (\in_array ($ process ['status ' ], self ::FAILED_PROCESS_STATUS , true )) {
215+ throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
216+ }
217+ if (self ::SUCESS_PROCESS_STATUS === $ process ['status ' ]) {
218+ $ downloadUrl = $ process ['details ' ]['download_url ' ];
219+ break ;
220+ }
221+ usleep (500000 );
222+ }
223+
224+ $ response = $ this ->client ->request ('GET ' , $ downloadUrl , ['buffer ' => false ]);
225+ if (200 !== $ response ->getStatusCode ()) {
226+ throw new ProviderException (\sprintf ('Unable to download translations file from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
227+ }
228+ $ zipFile = tempnam (sys_get_temp_dir (), 'lokalise ' );
229+ $ extractPath = $ zipFile .'.dir ' ;
230+ try {
231+ if (!$ h = @fopen ($ zipFile , 'w ' )) {
232+ throw new RuntimeException (error_get_last ()['message ' ] ?? 'Failed to create temporary file. ' );
233+ }
234+ foreach ($ this ->client ->stream ($ response ) as $ chunk ) {
235+ fwrite ($ h , $ chunk ->getContent ());
236+ }
237+ fclose ($ h );
238+
239+ $ zip = new \ZipArchive ();
240+ if (!$ zip ->open ($ zipFile )) {
241+ throw new RuntimeException ('Failed to open zipped translations from Lokalise. ' );
242+ }
243+
244+ try {
245+ if (!$ zip ->extractTo ($ extractPath )) {
246+ throw new RuntimeException ('Failed to unzip translations from Lokalize. ' );
247+ }
248+ } finally {
249+ $ zip ->close ();
250+ }
251+
252+ return $ this ->getZipContents ($ extractPath );
253+ } finally {
254+ if (is_resource ($ h )) {
255+ fclose ($ h );
256+ }
257+ @unlink ($ zipFile );
258+ $ this ->removeDir ($ extractPath );
259+ }
260+ }
261+
262+ private function getZipContents (string $ dir ): array
263+ {
264+ $ contents = [];
265+ foreach (scandir ($ dir ) as $ lang ) {
266+ if (\in_array ($ lang , ['. ' , '.. ' ], true )) {
267+ continue ;
268+ }
269+ $ path = $ dir .'/ ' .$ lang ;
270+ // Lokalise returns languages with "-" separator, we need to reformat them to "_" separator.
271+ $ lang = str_replace ('- ' , '_ ' , $ lang );
272+ foreach (scandir ($ path ) as $ name ) {
273+ if (!\in_array ($ name , ['. ' , '.. ' ], true )) {
274+ $ contents [$ lang ][$ name ]['content ' ] = file_get_contents ($ path .'/ ' .$ name );
275+ }
276+ }
277+ }
278+
279+ return $ contents ;
280+ }
281+
282+ private function removeDir (string $ dir ): void
283+ {
284+ if (!is_dir ($ dir )) {
285+ return ;
286+ }
287+ $ it = new \RecursiveDirectoryIterator ($ dir , \RecursiveDirectoryIterator::SKIP_DOTS );
288+ $ files = new \RecursiveIteratorIterator ($ it , \RecursiveIteratorIterator::CHILD_FIRST );
289+ foreach ($ files as $ file ) {
290+ if ($ file ->isDir ()) {
291+ rmdir ($ file ->getPathname ());
292+ } else {
293+ unlink ($ file ->getPathname ());
294+ }
295+ }
296+ rmdir ($ dir );
297+ }
298+
179299 private function createKeys (array $ keys , string $ domain ): array
180300 {
181301 $ keysToCreate = [];
0 commit comments