1 | <?php |
||||||
2 | |||||||
3 | namespace Printful\GettextCms; |
||||||
4 | |||||||
5 | use Gettext\Languages\Language; |
||||||
6 | use Gettext\Translation; |
||||||
7 | use Gettext\Translations; |
||||||
8 | use InvalidArgumentException; |
||||||
9 | use Printful\GettextCms\Exceptions\InvalidTranslationException; |
||||||
10 | use Printful\GettextCms\Interfaces\MessageRepositoryInterface; |
||||||
11 | use Printful\GettextCms\Structures\MessageItem; |
||||||
12 | |||||||
13 | /** |
||||||
14 | * Class handles all saving and retrieving logic of translations using the given repository |
||||||
15 | */ |
||||||
16 | class MessageStorage |
||||||
17 | { |
||||||
18 | const TYPE_FILE = 'file'; |
||||||
19 | const TYPE_DYNAMIC = 'dynamic'; |
||||||
20 | |||||||
21 | /** @var MessageRepositoryInterface */ |
||||||
22 | private $repository; |
||||||
23 | |||||||
24 | /** |
||||||
25 | * Cache for plural form counts |
||||||
26 | * @var array [locale => plural count, ..] |
||||||
27 | */ |
||||||
28 | private $pluralFormCache = []; |
||||||
29 | 44 | ||||||
30 | public function __construct(MessageRepositoryInterface $repository) |
||||||
31 | 44 | { |
|||||
32 | 44 | $this->repository = $repository; |
|||||
33 | } |
||||||
34 | |||||||
35 | /** |
||||||
36 | * Save translation object for a single domain and locale |
||||||
37 | * |
||||||
38 | * @param Translations $translations |
||||||
39 | * @throws InvalidTranslationException |
||||||
40 | 14 | */ |
|||||
41 | public function createOrUpdate(Translations $translations) |
||||||
42 | 14 | { |
|||||
43 | $this->validateHeaders($translations); |
||||||
44 | 12 | ||||||
45 | 12 | $locale = $translations->getLanguage(); |
|||||
46 | $domain = $translations->getDomain(); |
||||||
47 | 12 | ||||||
48 | 12 | foreach ($translations as $translation) { |
|||||
49 | $this->createOrUpdateSingle($locale, $domain, $translation); |
||||||
50 | 12 | } |
|||||
51 | } |
||||||
52 | |||||||
53 | /** |
||||||
54 | * @param Translations $translations |
||||||
55 | * @throws InvalidTranslationException |
||||||
56 | 14 | */ |
|||||
57 | private function validateHeaders(Translations $translations) |
||||||
58 | 14 | { |
|||||
59 | 1 | if (!$translations->getLanguage()) { |
|||||
60 | throw new InvalidTranslationException('Locale is missing'); |
||||||
61 | } |
||||||
62 | 13 | ||||||
63 | 1 | if (!$translations->getDomain()) { |
|||||
64 | throw new InvalidTranslationException('Domain is missing'); |
||||||
65 | 12 | } |
|||||
66 | } |
||||||
67 | |||||||
68 | /** |
||||||
69 | * Create or update a translation that is found in a file |
||||||
70 | * |
||||||
71 | * @param string $locale |
||||||
72 | * @param string $domain |
||||||
73 | * @param Translation $translation |
||||||
74 | * @return bool |
||||||
75 | 30 | */ |
|||||
76 | public function createOrUpdateSingle(string $locale, string $domain, Translation $translation): bool |
||||||
77 | 30 | { |
|||||
78 | return $this->createOrUpdateSingleWithType($locale, $domain, $translation, self::TYPE_FILE); |
||||||
79 | } |
||||||
80 | |||||||
81 | /** |
||||||
82 | * Create or update a translation that is dynamically generated |
||||||
83 | * |
||||||
84 | * @param string $locale |
||||||
85 | * @param string $domain |
||||||
86 | * @param Translation $translation |
||||||
87 | * @return bool |
||||||
88 | 6 | */ |
|||||
89 | public function createOrUpdateSingleDynamic(string $locale, string $domain, Translation $translation): bool |
||||||
90 | 6 | { |
|||||
91 | return $this->createOrUpdateSingleWithType($locale, $domain, $translation, self::TYPE_DYNAMIC); |
||||||
92 | } |
||||||
93 | |||||||
94 | /** |
||||||
95 | * Save a translation to repository by merging it to a previously saved version. |
||||||
96 | * |
||||||
97 | * @param string $locale |
||||||
98 | * @param string $domain |
||||||
99 | * @param Translation $translation |
||||||
100 | * @param string $type |
||||||
101 | * @return bool |
||||||
102 | 32 | */ |
|||||
103 | private function createOrUpdateSingleWithType( |
||||||
104 | string $locale, |
||||||
105 | string $domain, |
||||||
106 | Translation $translation, |
||||||
107 | string $type |
||||||
108 | ): bool { |
||||||
109 | 32 | // Make a clone so we don't modify the passed instance |
|||||
110 | $translation = $translation->getClone(); |
||||||
111 | 32 | ||||||
112 | $key = $this->getKey($locale, $domain, $translation); |
||||||
113 | 32 | ||||||
114 | $existingItem = $this->repository->getSingle($key); |
||||||
115 | 32 | ||||||
116 | 10 | if ($existingItem->exists()) { |
|||||
117 | 10 | $existingTranslation = $this->itemToTranslation($existingItem); |
|||||
118 | 10 | $translation->mergeWith($existingTranslation); |
|||||
119 | $item = $this->translationToItem($locale, $domain, $translation); |
||||||
120 | 32 | } else { |
|||||
121 | $item = $this->translationToItem($locale, $domain, $translation); |
||||||
122 | } |
||||||
123 | 32 | ||||||
124 | $item->isInJs = $item->isInJs ?: $existingItem->isInJs; |
||||||
125 | 32 | ||||||
126 | 6 | if ($type == self::TYPE_DYNAMIC || $existingItem->isDynamic) { |
|||||
127 | $item->isDynamic = true; |
||||||
128 | } |
||||||
129 | 32 | ||||||
130 | 30 | if ($type == self::TYPE_FILE || $item->isInJs || $existingItem->isInFile) { |
|||||
131 | $item->isInFile = true; |
||||||
132 | } |
||||||
133 | 32 | ||||||
134 | 32 | $item->hasOriginalTranslation = $translation->hasTranslation(); |
|||||
135 | $item->requiresTranslating = $this->requiresTranslating($locale, $translation); |
||||||
136 | 32 | ||||||
137 | $item->isDisabled = $item->isDisabled ?: (!$item->isInJs && !$item->isInFile && !$item->isDynamic); |
||||||
138 | 32 | ||||||
139 | if (!$this->hasChanged($existingItem, $item)) { |
||||||
140 | return true; |
||||||
141 | } |
||||||
142 | 32 | ||||||
143 | return $this->repository->save($item); |
||||||
144 | } |
||||||
145 | |||||||
146 | /** |
||||||
147 | * Save translation object for a single domain and locale |
||||||
148 | * |
||||||
149 | * @param Translations $translations |
||||||
150 | * @throws InvalidTranslationException |
||||||
151 | 2 | */ |
|||||
152 | public function saveTranslated(Translations $translations) |
||||||
153 | 2 | { |
|||||
154 | $this->validateHeaders($translations); |
||||||
155 | 2 | ||||||
156 | 2 | $locale = $translations->getLanguage(); |
|||||
157 | $domain = $translations->getDomain(); |
||||||
158 | 2 | ||||||
159 | 2 | foreach ($translations as $translation) { |
|||||
160 | $this->saveTranslatedSingle($locale, $domain, $translation); |
||||||
161 | 2 | } |
|||||
162 | } |
||||||
163 | |||||||
164 | /** |
||||||
165 | * Function for saving only translated values. |
||||||
166 | * This will not modify disabled state and will not create new entries in repository, only modifies existing |
||||||
167 | * |
||||||
168 | * @param string $locale |
||||||
169 | * @param string $domain |
||||||
170 | * @param Translation $translation |
||||||
171 | * @return bool |
||||||
172 | 5 | */ |
|||||
173 | public function saveTranslatedSingle(string $locale, string $domain, Translation $translation): bool |
||||||
174 | 5 | { |
|||||
175 | 1 | if (!$translation->hasTranslation()) { |
|||||
176 | return false; |
||||||
177 | } |
||||||
178 | 4 | ||||||
179 | $existingItem = $this->repository->getSingle($this->getKey($locale, $domain, $translation)); |
||||||
180 | 4 | ||||||
181 | 1 | if (!$existingItem->exists()) { |
|||||
182 | return false; |
||||||
183 | } |
||||||
184 | 3 | ||||||
185 | $existingTranslation = $this->itemToTranslation($existingItem); |
||||||
186 | 3 | ||||||
187 | $existingTranslation->setTranslation($translation->getTranslation()); |
||||||
188 | |||||||
189 | 3 | // Make sure we do not drop previous plural translations if current one does not contain one |
|||||
190 | $pluralTranslations = $translation->getPluralTranslations(); |
||||||
191 | 3 | ||||||
192 | 2 | if (!empty($pluralTranslations)) { |
|||||
193 | $existingTranslation->setPluralTranslations($pluralTranslations); |
||||||
194 | } |
||||||
195 | 3 | ||||||
196 | $item = $this->translationToItem($locale, $domain, $existingTranslation); |
||||||
197 | 3 | ||||||
198 | $item->hasOriginalTranslation = true; |
||||||
199 | 3 | // It's still possible that plurals are missing and this translation still needs work |
|||||
200 | $item->requiresTranslating = $this->requiresTranslating($locale, $translation); |
||||||
201 | 3 | ||||||
202 | if (!$this->hasChanged($existingItem, $item)) { |
||||||
203 | return true; |
||||||
204 | } |
||||||
205 | 3 | ||||||
206 | return $this->repository->save($item); |
||||||
207 | } |
||||||
208 | |||||||
209 | /** |
||||||
210 | * Check if some translations are missing (original or missing plural forms) |
||||||
211 | * |
||||||
212 | * @param string $locale |
||||||
213 | * @param Translation $translation |
||||||
214 | * @return bool |
||||||
215 | 32 | */ |
|||||
216 | private function requiresTranslating(string $locale, Translation $translation): bool |
||||||
217 | 32 | { |
|||||
218 | 14 | if (!$translation->hasTranslation()) { |
|||||
219 | return true; |
||||||
220 | } |
||||||
221 | 23 | ||||||
222 | 8 | if ($translation->hasPlural()) { |
|||||
223 | $translatedPluralCount = count(array_filter($translation->getPluralTranslations())); |
||||||
224 | 8 | // If there are less plural translations than language requires, this needs translating |
|||||
225 | return $this->getPluralCount($locale) !== $translatedPluralCount; |
||||||
226 | } |
||||||
227 | 17 | ||||||
228 | return false; |
||||||
229 | } |
||||||
230 | |||||||
231 | /** |
||||||
232 | * Get number of plural forms for this locale |
||||||
233 | * |
||||||
234 | * @param string $locale |
||||||
235 | * @return int |
||||||
236 | * @throws InvalidArgumentException Thrown in the locale is not correct |
||||||
237 | 8 | */ |
|||||
238 | private function getPluralCount(string $locale): int |
||||||
239 | 8 | { |
|||||
240 | 8 | if (!array_key_exists($locale, $this->pluralFormCache)) { |
|||||
241 | $info = Language::getById($locale); |
||||||
242 | |||||||
243 | 8 | // Minus one, because we do not count the original string as a plural |
|||||
244 | $this->pluralFormCache[$locale] = count($info->categories) - 1; |
||||||
245 | } |
||||||
246 | 8 | ||||||
247 | return $this->pluralFormCache[$locale]; |
||||||
248 | } |
||||||
249 | |||||||
250 | /** |
||||||
251 | * Generate a unique key for storage (basically a primary key) |
||||||
252 | * |
||||||
253 | * @param string $locale |
||||||
254 | * @param string $domain |
||||||
255 | * @param Translation $translation |
||||||
256 | * @return string |
||||||
257 | 33 | */ |
|||||
258 | private function getKey(string $locale, string $domain, Translation $translation): string |
||||||
259 | 33 | { |
|||||
260 | return md5($locale . '|' . $domain . '|' . $translation->getContext() . '|' . $translation->getOriginal()); |
||||||
261 | } |
||||||
262 | |||||||
263 | /** |
||||||
264 | * Convert a message item to a translation item |
||||||
265 | * |
||||||
266 | * @param MessageItem $item |
||||||
267 | * @return Translation |
||||||
268 | 28 | */ |
|||||
269 | private function itemToTranslation(MessageItem $item): Translation |
||||||
270 | 28 | { |
|||||
271 | $translation = new Translation($item->context, $item->original, $item->originalPlural); |
||||||
272 | 28 | ||||||
273 | 28 | $translation->setTranslation($item->translation); |
|||||
274 | 28 | $translation->setPluralTranslations($item->pluralTranslations); |
|||||
275 | $translation->setDisabled($item->isDisabled); |
||||||
276 | 28 | ||||||
277 | 8 | foreach ($item->references as $v) { |
|||||
278 | $translation->addReference($v[0], $v[1]); |
||||||
279 | } |
||||||
280 | 28 | ||||||
281 | 1 | foreach ($item->comments as $v) { |
|||||
282 | $translation->addComment($v); |
||||||
283 | } |
||||||
284 | 28 | ||||||
285 | 1 | foreach ($item->extractedComments as $v) { |
|||||
286 | $translation->addExtractedComment($v); |
||||||
287 | } |
||||||
288 | 28 | ||||||
289 | return $translation; |
||||||
290 | } |
||||||
291 | |||||||
292 | /** |
||||||
293 | * Convert a translation item to a message item |
||||||
294 | * |
||||||
295 | * @param string $locale |
||||||
296 | * @param string $domain |
||||||
297 | * @param Translation $translation |
||||||
298 | * @return MessageItem |
||||||
299 | 32 | */ |
|||||
300 | private function translationToItem(string $locale, string $domain, Translation $translation): MessageItem |
||||||
301 | 32 | { |
|||||
302 | $item = new MessageItem(); |
||||||
303 | 32 | ||||||
304 | 32 | $item->key = $this->getKey($locale, $domain, $translation); |
|||||
305 | 32 | $item->domain = $domain; |
|||||
306 | 32 | $item->locale = $locale; |
|||||
307 | 32 | $item->context = (string)$translation->getContext(); |
|||||
308 | 32 | $item->original = $translation->getOriginal(); |
|||||
309 | 32 | $item->translation = $translation->getTranslation(); |
|||||
310 | 32 | $item->originalPlural = $translation->getPlural(); |
|||||
311 | 32 | $item->pluralTranslations = $translation->getPluralTranslations(); |
|||||
312 | 32 | $item->references = $translation->getReferences(); |
|||||
313 | 32 | $item->comments = $translation->getComments(); |
|||||
314 | 32 | $item->extractedComments = $translation->getExtractedComments(); |
|||||
315 | $item->isDisabled = $translation->isDisabled(); |
||||||
316 | 32 | ||||||
317 | 8 | foreach ($item->references as $reference) { |
|||||
318 | 2 | if ($this->isJsFile($reference[0] ?? '')) { |
|||||
319 | 8 | $item->isInJs = true; |
|||||
320 | break; |
||||||
321 | } |
||||||
322 | } |
||||||
323 | 32 | ||||||
324 | return $item; |
||||||
325 | } |
||||||
326 | |||||||
327 | /** |
||||||
328 | * Check if this is considered a JS file. |
||||||
329 | * |
||||||
330 | * @param string $filename |
||||||
331 | * @return bool |
||||||
332 | 8 | */ |
|||||
333 | private function isJsFile(string $filename): bool |
||||||
334 | 8 | { |
|||||
335 | $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); |
||||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||||
336 | 8 | ||||||
337 | return in_array($extension, ['vue', 'js'], true); |
||||||
338 | } |
||||||
339 | |||||||
340 | /** |
||||||
341 | * All translations, including disabled, enabled and untranslated |
||||||
342 | * |
||||||
343 | * @param string $locale |
||||||
344 | * @param $domain |
||||||
345 | * @return Translations |
||||||
346 | 6 | */ |
|||||
347 | public function getAll(string $locale, $domain): Translations |
||||||
348 | 6 | { |
|||||
349 | 6 | return $this->convertItems( |
|||||
350 | 6 | $locale, |
|||||
351 | 6 | (string)$domain, |
|||||
352 | $this->repository->getAll($locale, (string)$domain) |
||||||
353 | ); |
||||||
354 | } |
||||||
355 | |||||||
356 | /** |
||||||
357 | * All enabled translations including untranslated |
||||||
358 | * |
||||||
359 | * @param string $locale |
||||||
360 | * @param $domain |
||||||
361 | * @return Translations |
||||||
362 | 7 | */ |
|||||
363 | public function getAllEnabled(string $locale, $domain): Translations |
||||||
364 | 7 | { |
|||||
365 | 7 | return $this->convertItems( |
|||||
366 | 7 | $locale, |
|||||
367 | 7 | (string)$domain, |
|||||
368 | $this->repository->getEnabled($locale, (string)$domain) |
||||||
369 | ); |
||||||
370 | } |
||||||
371 | |||||||
372 | /** |
||||||
373 | * Enabled and translated translations only |
||||||
374 | * |
||||||
375 | * @param string $locale |
||||||
376 | * @param $domain |
||||||
377 | * @return Translations |
||||||
378 | 12 | */ |
|||||
379 | public function getEnabledTranslated(string $locale, $domain): Translations |
||||||
380 | 12 | { |
|||||
381 | $items = $this->repository->getEnabledTranslated($locale, (string)$domain); |
||||||
382 | 12 | ||||||
383 | return $this->convertItems($locale, (string)$domain, $items); |
||||||
384 | } |
||||||
385 | |||||||
386 | /** |
||||||
387 | * Enabled and translated JS translations |
||||||
388 | * |
||||||
389 | * @param string $locale |
||||||
390 | * @param $domain |
||||||
391 | * @return Translations |
||||||
392 | 3 | */ |
|||||
393 | public function getEnabledTranslatedJs(string $locale, $domain): Translations |
||||||
394 | 3 | { |
|||||
395 | $items = $this->repository->getEnabledTranslatedJs($locale, (string)$domain); |
||||||
396 | 3 | ||||||
397 | return $this->convertItems($locale, (string)$domain, $items); |
||||||
398 | } |
||||||
399 | |||||||
400 | /** |
||||||
401 | * Enabled and translated translations only |
||||||
402 | * |
||||||
403 | * @param string $locale |
||||||
404 | * @param $domain |
||||||
405 | * @return Translations |
||||||
406 | 9 | */ |
|||||
407 | public function getRequiresTranslating(string $locale, $domain): Translations |
||||||
408 | 9 | { |
|||||
409 | 9 | return $this->convertItems( |
|||||
410 | 9 | $locale, |
|||||
411 | 9 | (string)$domain, |
|||||
412 | $this->repository->getRequiresTranslating($locale, (string)$domain) |
||||||
413 | ); |
||||||
414 | } |
||||||
415 | |||||||
416 | /** |
||||||
417 | * Converts message items to a translation object |
||||||
418 | * |
||||||
419 | * @param string $locale |
||||||
420 | * @param string|null $domain |
||||||
421 | * @param MessageItem[] $items |
||||||
422 | * @return Translations |
||||||
423 | 32 | */ |
|||||
424 | private function convertItems(string $locale, $domain, array $items): Translations |
||||||
425 | 32 | { |
|||||
426 | $domain = (string)$domain; |
||||||
427 | 32 | ||||||
428 | 32 | $translations = new Translations(); |
|||||
429 | 32 | $translations->setDomain($domain); |
|||||
430 | $translations->setLanguage($locale); |
||||||
431 | 32 | ||||||
432 | 27 | foreach ($items as $v) { |
|||||
433 | 27 | $translation = $this->itemToTranslation($v); |
|||||
434 | $translations[] = $translation; |
||||||
435 | } |
||||||
436 | 32 | ||||||
437 | return $translations; |
||||||
438 | } |
||||||
439 | |||||||
440 | /** |
||||||
441 | * Check if new item is different than the old one |
||||||
442 | * |
||||||
443 | * @param MessageItem $old |
||||||
444 | * @param MessageItem $new |
||||||
445 | * @return bool |
||||||
446 | 32 | */ |
|||||
447 | private function hasChanged(MessageItem $old, MessageItem $new): bool |
||||||
0 ignored issues
–
show
The parameter
$new is not used and could be removed.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for parameters that have been defined for a function or method, but which are not used in the method body. ![]() The parameter
$old is not used and could be removed.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for parameters that have been defined for a function or method, but which are not used in the method body. ![]() |
|||||||
448 | { |
||||||
449 | 32 | // TODO implement |
|||||
450 | return true; |
||||||
451 | } |
||||||
452 | |||||||
453 | /** |
||||||
454 | * Set all locale and domain messages as not present in files ("isFile" and "isInJs" fields should be set to false) |
||||||
455 | * |
||||||
456 | * @param string $locale |
||||||
457 | * @param string $domain |
||||||
458 | 5 | */ |
|||||
459 | public function setAllAsNotInFilesAndInJs(string $locale, string $domain) |
||||||
460 | 5 | { |
|||||
461 | 5 | $this->repository->setAllAsNotInFilesAndInJs($locale, $domain); |
|||||
462 | } |
||||||
463 | |||||||
464 | /** |
||||||
465 | * Set all locale and domain messages as not dynamic ("isDynamic" field should be set to false) |
||||||
466 | * |
||||||
467 | * @param string $locale |
||||||
468 | * @param string $domain |
||||||
469 | 6 | */ |
|||||
470 | public function setAllAsNotDynamic(string $locale, string $domain) |
||||||
471 | 6 | { |
|||||
472 | 6 | $this->repository->setAllAsNotDynamic($locale, $domain); |
|||||
473 | } |
||||||
474 | |||||||
475 | /** |
||||||
476 | * Set all messages as disabled which are not used (messages which filed "isDynamic", "isInFile" and "isInJs" all are false) |
||||||
477 | 8 | */ |
|||||
478 | public function disableUnused() |
||||||
479 | 8 | { |
|||||
480 | 8 | $this->repository->disableUnused(); |
|||||
481 | } |
||||||
482 | } |