Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
MultiLayerTranslator
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
5 / 5
13
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLocale
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLocale
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 translate
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 resolveTranslation
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2
3declare(strict_types=1);
4
5namespace App\Infrastructure\Translation\Service;
6
7use App\Domain\Translation\Repository\TranslationRecordRepositoryInterface;
8use App\Domain\Translation\Service\TranslatorInterface;
9use App\Domain\Translation\ValueObject\Language;
10
11 class MultiLayerTranslator implements TranslatorInterface
12{
13    private string $currentLocale = Language::DEFAULT_LOCALE;
14
15    /**
16     * Cache for resolved translations during the current request to avoid repeated DB calls.
17     * Format: ['category:locale:key' => 'translated_string']
18     *
19     * @var array<string, string>
20     */
21    private array $cache = [];
22
23    public function __construct(
24        private TranslationRecordRepositoryInterface $repository
25    ) {}
26
27    public function setLocale(string $locale): void
28    {
29        $this->currentLocale = (new Language($locale))->getCode();
30    }
31
32    public function getLocale(): string
33    {
34        return $this->currentLocale;
35    }
36
37    public function translate(string $key, string $category = 'global', ?string $locale = null): string
38    {
39        $targetLocale = $locale !== null ? (new Language($locale))->getCode() : $this->currentLocale;
40        $cacheKey = "{$category}:{$targetLocale}:{$key}";
41
42        if (isset($this->cache[$cacheKey])) {
43            return $this->cache[$cacheKey];
44        }
45
46        $translated = $this->resolveTranslation($key, $category, $targetLocale);
47        $this->cache[$cacheKey] = $translated;
48
49        return $translated;
50    }
51
52    private function resolveTranslation(string $key, string $category, string $targetLocale): string
53    {
54        // Layer 1: Module/Category match in target locale
55        $record = $this->repository->findByKeyCategoryLanguage($key, $category, $targetLocale);
56        if ($record !== null) {
57            return $record->getMessageContent();
58        }
59
60        // Layer 2: Global category match in target locale
61        if ($category !== 'global') {
62            $globalRecord = $this->repository->findByKeyCategoryLanguage($key, 'global', $targetLocale);
63            if ($globalRecord !== null) {
64                return $globalRecord->getMessageContent();
65            }
66        }
67
68        // Layer 3: Fallback language ('en')
69        if ($targetLocale !== Language::DEFAULT_LOCALE) {
70            $enRecord = $this->repository->findByKeyCategoryLanguage($key, $category, Language::DEFAULT_LOCALE)
71                ?? ($category !== 'global' ? $this->repository->findByKeyCategoryLanguage($key, 'global', Language::DEFAULT_LOCALE) : null);
72            if ($enRecord !== null) {
73                return $enRecord->getMessageContent();
74            }
75        }
76
77        // Layer 4: Raw key
78        return $key;
79    }
80}