Passed
Push — master ( 80a442...0afc76 )
by Samuel
02:02
created

Translator::translate()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 33
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 33
rs 8.4444
c 0
b 0
f 0
cc 8
nc 10
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Efabrica\Translatte;
6
7
use Efabrica\Translatte\Cache\ICache;
8
use Efabrica\Translatte\Cache\NullCache;
9
use Efabrica\Translatte\Resolver\IResolver;
10
use Efabrica\Translatte\Resolver\StaticResolver;
11
use Efabrica\Translatte\Resource\IResource;
12
use Nette\Localization\ITranslator;
13
use InvalidArgumentException;
14
15
class Translator implements ITranslator
16
{
17
    /** @var string */
18
    private $defaultLang;
19
20
    /** @var string  */
21
    private $lang;
22
23
    /** @var IResolver */
24
    private $resolver;
25
26
    /** @var ICache */
27
    private $cache;
28
29
    /** @var array */
30
    private $resources = [];
31
32
    /** @var array */
33
    private $fallbackLanguages = [];
34
35
    /** @var array */
36
    private $dictionaries = [];
37
38
    public function __construct(
39
        string $defaultLang,
40
        IResolver $resolver = null,
41
        ICache $cache = null
42
    ) {
43
        $this->defaultLang = $defaultLang;
44
        $this->resolver = $resolver ?: new StaticResolver($defaultLang);
45
        $this->cache = $cache ?: new NullCache();
46
    }
47
48
    /**
49
     * Fallback languages will be used as waterfall, first with valid result is used
50
     * @param array $fallbackLanguages
51
     */
52
    public function setFallbackLanguages(array $fallbackLanguages): void
53
    {
54
        $this->fallbackLanguages = $fallbackLanguages;
55
    }
56
57
    /**
58
     * Add new resource to parse translations from
59
     * @param IResource $resource
60
     * @return Translator
61
     */
62
    public function addResource(IResource $resource): self
63
    {
64
        $this->resources[] = $resource;
65
        return $this;
66
    }
67
68
    /**
69
     * Provide translation
70
     * @param string|int $message
71
     * @param mixed ...$parameters
72
     * @return string
73
     */
74
    public function translate($message, ...$parameters): string
75
    {
76
        // translate($message, int $count, array $params, string $lang = null)
77
        // translate($message, array $params, string $lang = null)
78
79
        $message = (string) $message;
80
        list($count, $params, $lang) = array_values($this->parseParameters($parameters));
81
82
        // If wrong input arguments passed, return message key
83
        if (!is_int($count) || !is_array($params) || !is_string($lang)) {
84
            return $message; // @ maybe throw exception?
85
        }
86
87
        $translation = $this->getDictionary($lang)->findTranslation($message);
88
        if ($translation === null) {
89
            // Try find translation in fallback languages
90
            foreach ($this->fallbackLanguages as $fallbackLanguage) {
91
                $translation = $this->getDictionary($fallbackLanguage)->findTranslation($message);
92
                if ($translation !== null) {
93
                    break;
94
                }
95
            }
96
97
            // If translation not found in base either fallback languages return message key
98
            if ($translation === null) {
99
                return $message;
100
            }
101
        }
102
103
        $translation = $this->selectRightPluralForm($translation, $lang, $count);
104
        $translation = $this->replaceParams($translation, $params);
105
106
        return $translation;
107
    }
108
109
    /**
110
     * Select right plural form based on selected language
111
     * @param string $translation
112
     * @param string $lang
113
     * @param int $count
114
     * @return string
115
     */
116
    private function selectRightPluralForm(string $translation, string $lang, int $count): string
117
    {
118
        $exploded = explode('|', $translation);
119
        if (count($exploded) === 1) {
120
            return $translation;
121
        }
122
123
        $pluralForm = PluralForm::get($count, $lang);
124
        return isset($exploded[$pluralForm]) ? $exploded[$pluralForm] : $exploded[0];
125
    }
126
127
    /**
128
     * Replace parameters in translation string
129
     * @param string $translation
130
     * @param array $params
131
     * @return string
132
     */
133
    private function replaceParams(string $translation, array $params): string
134
    {
135
        $transParams = [];
136
        foreach ($params as $key => $value) {
137
            $transParams["%" . $key . "%"] = $value;
138
        }
139
140
        return strtr($translation, $transParams);
141
    }
142
143
    /**
144
     * Parse translation input parameters
145
     * @param array $parameters
146
     * @return array
147
     */
148
    private function parseParameters(array $parameters): array
149
    {
150
        if (!count($parameters)) {
151
            return [
152
                'count' => 1,
153
                'params' => ['count' => 1],
154
                'lang' => $this->getResolvedLang()
155
            ];
156
        }
157
158
        if (is_array($parameters[0])) {
159
            return [
160
                'count' => isset($parameters[0]['count']) ? $parameters[0]['count'] : 1,
161
                'params' => $parameters[0],
162
                'lang' => array_key_exists(1, $parameters) ? $parameters[1] : $this->getResolvedLang()
163
            ];
164
        }
165
166
        $params = array_key_exists(1, $parameters) ? $parameters[1] : [];
167
        if (!isset($params['count'])) {
168
            $params['count'] = $parameters[0];
169
        }
170
171
        return [
172
            'count' => $parameters[0],
173
            'params' => $params,
174
            'lang' => array_key_exists(2, $parameters) ? $parameters[2] : $this->getResolvedLang()
175
        ];
176
    }
177
178
    /**
179
     * Use resolvers to resolve language to use
180
     * @return string
181
     */
182
    private function getResolvedLang(): string
183
    {
184
        if ($this->lang === null) {
185
            $resolvedLang = $this->resolver->resolve();
186
            $this->lang = $resolvedLang !== null ? $resolvedLang : $this->defaultLang;
187
        }
188
        return $this->lang;
189
    }
190
191
    /**
192
     * Prepare and return dictionary ready to use
193
     * @param string $lang
194
     * @return Dictionary
195
     */
196
    private function getDictionary(string $lang): Dictionary
197
    {
198
        if (array_key_exists($lang, $this->dictionaries)) {
199
            return $this->dictionaries[$lang];
200
        }
201
202
        $dictionary = $this->cache->load($lang);
203
        if ($dictionary !== null) {
204
            $this->dictionaries[$lang] = $dictionary;
205
            return $this->dictionaries[$lang];
206
        }
207
208
        $this->dictionaries[$lang] = new Dictionary($lang);
209
        foreach ($this->resources as $resource) {
210
            $dictionaries = $resource->load($lang);
211
            foreach ($dictionaries as $dictionary) {
212
                if (!$dictionary instanceof Dictionary) {
213
                    throw new InvalidArgumentException(sprintf("%s expected. Resource returned %s", Dictionary::class, get_class($dictionary)));
214
                }
215
                $this->dictionaries[$lang]->extend($dictionary);
216
            }
217
        }
218
219
        return $this->dictionaries[$lang];
220
    }
221
}
222