MessageStorage::requiresTranslating()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 13
ccs 6
cts 6
cp 1
rs 10
cc 3
nc 3
nop 2
crap 3
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
It seems like pathinfo($filename, Prin...Cms\PATHINFO_EXTENSION) can also be of type array; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

335
        $extension = strtolower(/** @scrutinizer ignore-type */ pathinfo($filename, PATHINFO_EXTENSION));
Loading history...
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
Unused Code introduced by
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 ignore-unused  annotation

447
    private function hasChanged(MessageItem $old, /** @scrutinizer ignore-unused */ MessageItem $new): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
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 ignore-unused  annotation

447
    private function hasChanged(/** @scrutinizer ignore-unused */ MessageItem $old, MessageItem $new): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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
}