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 ERROR_STATUS = [
36+ 'PROJECT_TOO_BIG ' => 413 ,
37+ ];
3438
3539 private HttpClientInterface $ client ;
3640 private LoaderInterface $ loader ;
@@ -96,7 +100,7 @@ public function write(TranslatorBagInterface $translatorBag): void
96100 public function read (array $ domains , array $ locales ): TranslatorBag
97101 {
98102 $ translatorBag = new TranslatorBag ();
99- $ translations = $ this ->exportFiles ($ locales , $ domains );
103+ $ translations = $ this ->exportFilesAsync ($ locales , $ domains );
100104
101105 foreach ($ translations as $ locale => $ files ) {
102106 foreach ($ files as $ filename => $ content ) {
@@ -165,6 +169,9 @@ private function exportFiles(array $locales, array $domains): array
165169 }
166170
167171 if (200 !== $ response ->getStatusCode ()) {
172+ if (($ errorCode = $ responseContent ['error ' ]['code ' ] ?? null ) && self ::ERROR_STATUS ['PROJECT_TOO_BIG ' ] === $ errorCode && \extension_loaded ('zip ' )) {
173+ return $ this ->exportFilesAsync ($ locales , $ domains );
174+ }
168175 throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
169176 }
170177
@@ -176,6 +183,110 @@ private function exportFiles(array $locales, array $domains): array
176183 return array_combine ($ reformattedLanguages , $ responseContent ['files ' ]);
177184 }
178185
186+ /**
187+ * @see https://developers.lokalise.com/reference/download-files-async
188+ */
189+ private function exportFilesAsync (array $ locales , array $ domains ): array
190+ {
191+ $ response = $ this ->client ->request ('POST ' , 'files/async-download ' , [
192+ 'json ' => [
193+ 'format ' => 'symfony_xliff ' ,
194+ 'original_filenames ' => true ,
195+ 'filter_langs ' => array_values ($ locales ),
196+ 'filter_filenames ' => array_map ($ this ->getLokaliseFilenameFromDomain (...), $ domains ),
197+ 'export_empty_as ' => 'skip ' ,
198+ 'replace_breaks ' => false ,
199+ ],
200+ ]);
201+
202+ if (200 !== $ response ->getStatusCode ()) {
203+ throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
204+ }
205+
206+ $ processId = $ response ->toArray ()['process_id ' ];
207+ $ attempt = 0 ;
208+ while (true ) {
209+ $ response = $ this ->client ->request ('GET ' , \sprintf ('processes/%s ' , $ processId ));
210+ $ process = $ response ->toArray ()['process ' ];
211+ if ('failed ' === $ process ['status ' ]) {
212+ throw new ProviderException (\sprintf ('Unable to export translations from Lokalise: "%s". ' , $ response ->getContent (false )), $ response );
213+ }
214+ if ('finished ' === $ process ['status ' ]) {
215+ $ downloadUrl = $ process ['details ' ]['download_url ' ];
216+ break ;
217+ }
218+ ++$ attempt ;
219+ usleep (500000 * $ attempt );
220+ }
221+
222+ try {
223+ $ newfile = \sprintf ('%s%s%s.zip ' , sys_get_temp_dir (), \DIRECTORY_SEPARATOR , uniqid ());
224+ $ extractPath = \sprintf ('%s%s%s ' , sys_get_temp_dir (), \DIRECTORY_SEPARATOR , uniqid ());
225+
226+ if (!copy ($ downloadUrl , $ newfile )) {
227+ throw new LogicException (\sprintf ('failed to copy "%s". ' , $ downloadUrl ));
228+ }
229+
230+ $ zip = new \ZipArchive ();
231+ if (!$ zip ->open ($ newfile , \ZipArchive::CREATE )) {
232+ throw new LogicException (\sprintf ('failed to open zip file "%s". ' , $ newfile ));
233+ }
234+
235+ try {
236+ if (!$ zip ->extractTo ($ extractPath )) {
237+ throw new LogicException (\sprintf ('failed to extract content from zip file "%s". ' , $ newfile ));
238+ }
239+ } finally {
240+ $ zip ->close ();
241+ }
242+
243+ $ fileContents = $ this ->getFileContents ($ extractPath );
244+
245+ // Lokalise returns languages with "-" separator, we need to reformat them to "_" separator.
246+ /** @var array<array-key, array-key> $reformattedLanguages */
247+ $ reformattedLanguages = array_map (function ($ language ) {
248+ return str_replace ('- ' , '_ ' , $ language );
249+ }, array_keys ($ fileContents ));
250+
251+ return array_combine ($ reformattedLanguages , $ fileContents );
252+ } finally {
253+ unlink ($ newfile );
254+ $ this ->removeDir ($ extractPath );
255+ }
256+ }
257+
258+ private function getFileContents (string $ dirName , string $ baseDirName = '' ): array
259+ {
260+ $ fileContents = [];
261+ foreach (scandir ($ dirName ) as $ filename ) {
262+ if (\in_array ($ filename , ['. ' , '.. ' ])) {
263+ continue ;
264+ }
265+ $ path = \sprintf ('%s%s%s ' , $ dirName , \DIRECTORY_SEPARATOR , $ filename );
266+ if (is_dir ($ path )) {
267+ $ fileContents = array_merge ($ fileContents , $ this ->getFileContents ($ path , $ filename ));
268+ continue ;
269+ }
270+ $ fileContents [$ baseDirName ][$ filename ]['content ' ] = file_get_contents ($ path );
271+ }
272+
273+ return $ fileContents ;
274+ }
275+
276+ private function removeDir (string $ dir ): void
277+ {
278+ $ it = new \RecursiveDirectoryIterator ($ dir , \RecursiveDirectoryIterator::SKIP_DOTS );
279+ $ files = new \RecursiveIteratorIterator ($ it , \RecursiveIteratorIterator::CHILD_FIRST );
280+ foreach ($ files as $ file ) {
281+ if ($ file ->isDir ()) {
282+ rmdir ($ file ->getPathname ());
283+ } else {
284+ unlink ($ file ->getPathname ());
285+ }
286+ }
287+ rmdir ($ dir );
288+ }
289+
179290 private function createKeys (array $ keys , string $ domain ): array
180291 {
181292 $ keysToCreate = [];
0 commit comments