1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Happyr\TranslationBundle\Service; |
4
|
|
|
|
5
|
|
|
use Happyr\TranslationBundle\Exception\HappyrTranslationException; |
6
|
|
|
use Happyr\TranslationBundle\Exception\HttpException; |
7
|
|
|
use Happyr\TranslationBundle\Http\RequestManager; |
8
|
|
|
use Happyr\TranslationBundle\Model\Message; |
9
|
|
|
use Happyr\TranslationBundle\Translation\FilesystemUpdater; |
10
|
|
|
use Symfony\Component\Filesystem\Exception\FileNotFoundException; |
11
|
|
|
use Symfony\Component\Translation\TranslatorInterface; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* @author Tobias Nyholm <[email protected]> |
15
|
|
|
*/ |
16
|
|
|
class Loco implements TranslationServiceInterface |
17
|
|
|
{ |
18
|
|
|
const BASE_URL = 'https://localise.biz/api/'; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* @var RequestManager |
22
|
|
|
*/ |
23
|
|
|
private $requestManager; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var array projects |
27
|
|
|
*/ |
28
|
|
|
private $projects; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var FilesystemUpdater filesystemService |
32
|
|
|
*/ |
33
|
|
|
private $filesystemService; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var TranslatorInterface filesystemService |
37
|
|
|
*/ |
38
|
|
|
private $translator; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @param TranslatorInterface $translator |
42
|
|
|
* @param RequestManager $requestManager |
43
|
|
|
* @param FilesystemUpdater $fs |
44
|
|
|
* @param array $projects |
45
|
|
|
*/ |
46
|
1 |
|
public function __construct(RequestManager $requestManager, FilesystemUpdater $fs, TranslatorInterface $translator, array $projects) |
47
|
|
|
{ |
48
|
1 |
|
$this->translator = $translator; |
49
|
1 |
|
$this->requestManager = $requestManager; |
50
|
1 |
|
$this->projects = $projects; |
51
|
1 |
|
$this->filesystemService = $fs; |
52
|
1 |
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @param $key |
56
|
|
|
* @param $method |
57
|
|
|
* @param $resource |
58
|
|
|
* @param null $body |
59
|
|
|
* @param string $type |
60
|
|
|
* @param array $extraQuery |
61
|
|
|
* @return array |
62
|
|
|
* @throws HttpException |
63
|
|
|
*/ |
64
|
|
|
protected function makeApiRequest($key, $method, $resource, $body = null, $type = 'form', $extraQuery = array()) |
65
|
|
|
{ |
66
|
|
|
$headers = array(); |
67
|
|
|
if ($body !== null) { |
68
|
|
|
if ($type === 'form') { |
69
|
|
|
if (is_array($body)) { |
70
|
|
|
$body = http_build_query($body); |
71
|
|
|
} |
72
|
|
|
$headers['Content-Type'] = 'application/x-www-form-urlencoded'; |
73
|
|
|
} elseif ($type === 'json') { |
74
|
|
|
$body = json_encode($body); |
75
|
|
|
$headers['Content-Type'] = 'application/json'; |
76
|
|
|
} |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
$query = array_merge($extraQuery, ['key' => $key]); |
80
|
|
|
$url = self::BASE_URL . $resource . '?' . http_build_query($query); |
81
|
|
|
|
82
|
|
|
return $this->requestManager->send($method, $url, $body, $headers); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Fetch a translation form Loco. |
87
|
|
|
* |
88
|
|
|
* @param Message $message |
89
|
|
|
*/ |
90
|
|
|
public function fetchTranslation(Message $message, $updateFs = false) |
91
|
|
|
{ |
92
|
|
|
$project = $this->getProject($message); |
93
|
|
|
|
94
|
|
|
try { |
95
|
|
|
$resource = sprintf('translations/%s/%s', $message->getId(), $message->getLocale()); |
96
|
|
|
$response = $this->makeApiRequest($project['api_key'], 'GET', $resource); |
97
|
|
|
} catch (HttpException $e) { |
98
|
|
|
if ($e->getCode() === 404) { |
99
|
|
|
//Message does not exist |
100
|
|
|
return; |
101
|
|
|
} |
102
|
|
|
throw $e; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
$logoTranslation = $response['translation']; |
106
|
|
|
$messageTranslation = $message->getTranslation(); |
107
|
|
|
$message->setTranslation($logoTranslation); |
108
|
|
|
|
109
|
|
|
// update filesystem |
110
|
|
|
if ($updateFs && $logoTranslation !== $messageTranslation) { |
111
|
|
|
$this->filesystemService->updateMessageCatalog([$message]); |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
return $logoTranslation; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Update the translation in Loco. |
119
|
|
|
* |
120
|
|
|
* @param Message $message |
121
|
|
|
*/ |
122
|
|
|
public function updateTranslation(Message $message) |
123
|
|
|
{ |
124
|
|
|
$project = $this->getProject($message); |
125
|
|
|
|
126
|
|
|
try { |
127
|
|
|
$resource = sprintf('translations/%s/%s', $message->getId(), $message->getLocale()); |
128
|
|
|
$this->makeApiRequest($project['api_key'], 'POST', $resource, $message->getTranslation()); |
129
|
|
|
} catch (HttpException $e) { |
130
|
|
|
if ($e->getCode() === 404) { |
131
|
|
|
//Asset does not exist |
132
|
|
|
if ($this->createAsset($message)) { |
133
|
|
|
//Try again |
134
|
|
|
return $this->updateTranslation($message); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
return false; |
138
|
|
|
} |
139
|
|
|
throw $e; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
$this->filesystemService->updateMessageCatalog([$message]); |
143
|
|
|
|
144
|
|
|
return true; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* If there is something wrong with the translation, please flag it. |
149
|
|
|
* |
150
|
|
|
* @param Message $message |
151
|
|
|
* @param int $type 0: Fuzzy, 1: Incorrect, 2: Provisional, 3: Unapproved, 4: Incomplete |
152
|
|
|
* |
153
|
|
|
* @return bool |
154
|
|
|
*/ |
155
|
|
|
public function flagTranslation(Message $message, $type = 0) |
156
|
|
|
{ |
157
|
|
|
$project = $this->getProject($message); |
158
|
|
|
$flags = ['fuzzy', 'incorrect', 'provisional', 'unapproved', 'incomplete']; |
159
|
|
|
|
160
|
|
|
try { |
161
|
|
|
$resource = sprintf('translations/%s/%s/flag', $message->getId(), $message->getLocale()); |
162
|
|
|
$this->makeApiRequest($project['api_key'], 'POST', $resource, ['flag' => $flags[$type]]); |
163
|
|
|
} catch (HttpException $e) { |
164
|
|
|
if ($e->getCode() === 404) { |
165
|
|
|
//Message does not exist |
166
|
|
|
return false; |
167
|
|
|
} |
168
|
|
|
throw $e; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
return true; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* Create a new asset in Loco. |
176
|
|
|
* |
177
|
|
|
* @param Message $message |
178
|
|
|
* |
179
|
|
|
* @return bool |
180
|
|
|
*/ |
181
|
|
|
public function createAsset(Message $message) |
182
|
|
|
{ |
183
|
|
|
$project = $this->getProject($message); |
184
|
|
|
|
185
|
|
|
try { |
186
|
|
|
$response = $this->makeApiRequest($project['api_key'], 'POST', 'assets', [ |
187
|
|
|
'id' => $message->getId(), |
188
|
|
|
'name' => $message->getId(), |
189
|
|
|
'type' => 'text', |
190
|
|
|
// Tell Loco not to translate the asset |
191
|
|
|
'default' => 'untranslated', |
192
|
|
|
]); |
193
|
|
|
|
194
|
|
|
if ($message->hasParameters()) { |
195
|
|
|
// Send those parameter as a note to Loco |
196
|
|
|
$notes = ''; |
197
|
|
|
foreach ($message->getParameters() as $key => $value) { |
198
|
|
|
if (!is_array($value)) { |
199
|
|
|
$notes .= 'Parameter: ' . $key . ' (i.e. : ' . $value . ")\n"; |
200
|
|
|
} else { |
201
|
|
|
foreach ($value as $k => $v) { |
202
|
|
|
$notes .= 'Parameter: ' . $k . ' (i.e. : ' . $v . ")\n"; |
203
|
|
|
} |
204
|
|
|
} |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
$resource = sprintf('assets/%s.json', $message->getId()); |
208
|
|
|
$this->makeApiRequest($project['api_key'], 'PATCH', $resource, ['notes' => $notes], 'json'); |
209
|
|
|
} |
210
|
|
|
} catch (HttpException $e) { |
211
|
|
|
if ($e->getCode() === 409) { |
212
|
|
|
//conflict.. ignore |
213
|
|
|
return false; |
214
|
|
|
} |
215
|
|
|
throw $e; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
// if this project has multiple domains. Make sure to tag it |
219
|
|
|
if (!empty($project['domains'])) { |
220
|
|
|
$this->addTagToAsset($project, $response['id'], $message->getDomain()); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
return true; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* @param Message $message |
228
|
|
|
* |
229
|
|
|
* @return array |
230
|
|
|
*/ |
231
|
|
|
protected function getProject(Message $message) |
232
|
|
|
{ |
233
|
|
|
if (isset($this->projects[$message->getDomain()])) { |
234
|
|
|
return $this->projects[$message->getDomain()]; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
// Return the first project that has the correct domain and locale |
238
|
|
|
foreach ($this->projects as $project) { |
239
|
|
|
if (in_array($message->getDomain(), $project['domains'])) { |
240
|
|
|
if (in_array($message->getLocale(), $project['locales'])) { |
241
|
|
|
return $project; |
242
|
|
|
} |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* @param $project |
249
|
|
|
* @param $messageId |
250
|
|
|
* @param $domain |
251
|
|
|
*/ |
252
|
|
|
protected function addTagToAsset($project, $messageId, $domain) |
253
|
|
|
{ |
254
|
|
|
$resource = sprintf('assets/%s/tags', $messageId); |
255
|
|
|
$this->makeApiRequest($project['api_key'], 'POST', $resource, ['name' => $domain]); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* Download all the translations from Loco. This will replace all the local files. |
260
|
|
|
* This is a quick method of getting all the latest translations and assets. |
261
|
|
|
*/ |
262
|
|
|
public function downloadAllTranslations() |
263
|
|
|
{ |
264
|
|
|
$data = []; |
265
|
|
|
foreach ($this->projects as $name => $config) { |
266
|
|
|
if (empty($config['domains'])) { |
267
|
|
|
$this->getUrls($data, $config, $name, false); |
268
|
|
|
} else { |
269
|
|
|
foreach ($config['domains'] as $domain) { |
270
|
|
|
$this->getUrls($data, $config, $domain, true); |
271
|
|
|
} |
272
|
|
|
} |
273
|
|
|
} |
274
|
|
|
$this->requestManager->downloadFiles($this->filesystemService, $data); |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Upload all the translations from the symfony project into Loco. This will override |
279
|
|
|
* every changed strings in loco |
280
|
|
|
*/ |
281
|
|
View Code Duplication |
public function uploadAllTranslations() |
|
|
|
|
282
|
|
|
{ |
283
|
|
|
foreach ($this->projects as $name => $config) { |
284
|
|
|
if (empty($config['domains'])) { |
285
|
|
|
$this->doUploadDomains($config, $name, false); |
286
|
|
|
} else { |
287
|
|
|
foreach ($config['domains'] as $domain) { |
288
|
|
|
$this->doUploadDomains($config, $domain, true); |
289
|
|
|
} |
290
|
|
|
} |
291
|
|
|
} |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
/** |
295
|
|
|
* @param array $config |
296
|
|
|
* @param $domain |
297
|
|
|
* @param $useDomainAsFilter |
298
|
|
|
*/ |
299
|
|
|
protected function doUploadDomains(array &$config, $domain, $useDomainAsFilter) |
300
|
|
|
{ |
301
|
|
|
$query = $this->getExportQueryParams($config['api_key']); |
302
|
|
|
|
303
|
|
|
if ($useDomainAsFilter) { |
304
|
|
|
$query['filter'] = $domain; |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
foreach ($config['locales'] as $locale) { |
308
|
|
|
$extension = $this->filesystemService->getFileExtension(); |
309
|
|
|
$file = $this->filesystemService->getTargetDir(); |
310
|
|
|
$file .= sprintf('/%s.%s.%s', $domain, $locale, $extension); |
311
|
|
|
|
312
|
|
|
if (is_file($file)) { |
313
|
|
|
$query = [ |
314
|
|
|
'index' => 'id', |
315
|
|
|
'tag' => $domain, |
316
|
|
|
'locale'=> $locale |
317
|
|
|
]; |
318
|
|
|
|
319
|
|
|
$resource = sprintf('import/%s', $extension); |
320
|
|
|
$response = $this->makeApiRequest($config['api_key'], 'POST', $resource, file_get_contents($file), 'form', $query); |
321
|
|
|
$this->flatten($response); |
322
|
|
|
} else { |
323
|
|
|
throw new FileNotFoundException(sprintf("Can't find %s file, perhaps you should generate the translations file ?", $file)); |
324
|
|
|
} |
325
|
|
|
} |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* Synchronize all the translations with Loco. This will keep placeholders. This function is slower |
330
|
|
|
* than just to download the translations. |
331
|
|
|
*/ |
332
|
|
View Code Duplication |
public function synchronizeAllTranslations() |
|
|
|
|
333
|
|
|
{ |
334
|
|
|
foreach ($this->projects as $name => $config) { |
335
|
|
|
if (empty($config['domains'])) { |
336
|
|
|
$this->doSynchronizeDomain($config, $name, false); |
337
|
|
|
} else { |
338
|
|
|
foreach ($config['domains'] as $domain) { |
339
|
|
|
$this->doSynchronizeDomain($config, $domain, true); |
340
|
|
|
} |
341
|
|
|
} |
342
|
|
|
} |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
/** |
346
|
|
|
* @param array $config |
347
|
|
|
* @param $domain |
348
|
|
|
* @param $useDomainAsFilter |
349
|
|
|
*/ |
350
|
|
|
protected function doSynchronizeDomain(array &$config, $domain, $useDomainAsFilter) |
351
|
|
|
{ |
352
|
|
|
$query = $this->getExportQueryParams($config['api_key']); |
353
|
|
|
|
354
|
|
|
if ($useDomainAsFilter) { |
355
|
|
|
$query['filter'] = $domain; |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
foreach ($config['locales'] as $locale) { |
359
|
|
|
$resource = sprintf('export/locale/%s.%s', $locale, 'json'); |
360
|
|
|
$response = $this->makeApiRequest($config['api_key'], 'GET', $resource, ['query' => $query]); |
361
|
|
|
|
362
|
|
|
$this->flatten($response); |
363
|
|
|
|
364
|
|
|
$messages = array(); |
365
|
|
|
foreach ($response as $id => $translation) { |
366
|
|
|
$messages[] = new Message([ |
367
|
|
|
'count' => 1, |
368
|
|
|
'domain' => $domain, |
369
|
|
|
'id' => $id, |
370
|
|
|
'locale' => $locale, |
371
|
|
|
'state' => 1, |
372
|
|
|
'translation' => $translation, |
373
|
|
|
]); |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
$this->filesystemService->updateMessageCatalog($messages); |
377
|
|
|
} |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* Flattens an nested array of translations. |
382
|
|
|
* |
383
|
|
|
* The scheme used is: |
384
|
|
|
* 'key' => array('key2' => array('key3' => 'value')) |
385
|
|
|
* Becomes: |
386
|
|
|
* 'key.key2.key3' => 'value' |
387
|
|
|
* |
388
|
|
|
* This function takes an array by reference and will modify it |
389
|
|
|
* |
390
|
|
|
* @param array &$messages The array that will be flattened |
391
|
|
|
* @param array $subnode Current subnode being parsed, used internally for recursive calls |
392
|
|
|
* @param string $path Current path being parsed, used internally for recursive calls |
393
|
|
|
*/ |
394
|
|
|
private function flatten(array &$messages, array $subnode = null, $path = null) |
395
|
|
|
{ |
396
|
|
|
if (null === $subnode) { |
397
|
|
|
$subnode = &$messages; |
398
|
|
|
} |
399
|
|
|
foreach ($subnode as $key => $value) { |
400
|
|
|
if (is_array($value)) { |
401
|
|
|
$nodePath = $path ? $path . '.' . $key : $key; |
402
|
|
|
$this->flatten($messages, $value, $nodePath); |
403
|
|
|
if (null === $path) { |
404
|
|
|
unset($messages[$key]); |
405
|
|
|
} |
406
|
|
|
} elseif (null !== $path) { |
407
|
|
|
$messages[$path . '.' . $key] = $value; |
408
|
|
|
} |
409
|
|
|
} |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
/** |
413
|
|
|
* @param array $data |
414
|
|
|
* @param array $config |
415
|
|
|
* @param string $domain |
416
|
|
|
* @param bool $useDomainAsFilter |
417
|
|
|
*/ |
418
|
|
|
protected function getUrls(array &$data, array $config, $domain, $useDomainAsFilter) |
419
|
|
|
{ |
420
|
|
|
$query = $this->getExportQueryParams($config['api_key']); |
421
|
|
|
|
422
|
|
|
if ($useDomainAsFilter) { |
423
|
|
|
$query['filter'] = $domain; |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
foreach ($config['locales'] as $locale) { |
427
|
|
|
// Build url |
428
|
|
|
$url = sprintf('%sexport/locale/%s.%s?%s', self::BASE_URL, $locale, $this->filesystemService->getFileExtension(), http_build_query($query)); |
429
|
|
|
$fileName = sprintf('%s.%s.%s', $domain, $locale, $this->filesystemService->getFileExtension()); |
430
|
|
|
|
431
|
|
|
$data[$url] = $fileName; |
432
|
|
|
} |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
/** |
436
|
|
|
* @param array $config |
|
|
|
|
437
|
|
|
* |
438
|
|
|
* @return array |
439
|
|
|
*/ |
440
|
|
|
private function getExportQueryParams($key) |
441
|
|
|
{ |
442
|
|
|
$data = array( |
443
|
|
|
'index' => 'id', |
444
|
|
|
'status' => 'translated', |
445
|
|
|
'key' => $key, |
446
|
|
|
); |
447
|
|
|
switch ($this->filesystemService->getFileExtension()) { |
448
|
|
|
case 'php': |
449
|
|
|
$data['format'] = 'zend'; // 'Zend' will give us a flat array |
450
|
|
|
break; |
451
|
|
|
case 'xlf': |
452
|
|
|
default: |
453
|
|
|
$data['format'] = 'symfony'; |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
return $data; |
457
|
|
|
} |
458
|
|
|
} |
459
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.