Completed
Push — master ( df7c6a...d7d222 )
by
unknown
20s queued 16s
created

Translator::reset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 2
c 3
b 0
f 0
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
cc 1
nc 1
nop 0
crap 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 InvalidArgumentException;
13
use Nette\Localization\ITranslator;
14
use Nette\Utils\Arrays;
15
16
class Translator implements ITranslator
17
{
18
    /** @var string */
19
    private $defaultLang;
20
21
    /** @var string */
22
    private $lang;
23
24
    /** @var IResolver */
25
    private $resolver;
26
27
    /** @var ICache */
28
    private $cache;
29
30
    /** @var IResource[] */
31
    private $resources = [];
32
33
    /** @var array */
34
    private $fallbackLanguages = [];
35
36
    /** @var Dictionary[] */
37
    private $dictionaries = [];
38 15
39
    /** @var array */
40
    public $onTranslate = [];
41
42
    public function __construct(
43 15
        string $defaultLang,
44 15
        IResolver $resolver = null,
45 15
        ICache $cache = null
46 15
    ) {
47
        $this->defaultLang = $defaultLang;
48
        $this->resolver = $resolver ?: new StaticResolver($defaultLang);
49
        $this->cache = $cache ?: new NullCache();
50
    }
51
52
    /**
53
     * Fallback languages will be used as waterfall, first with valid result is used
54
     * @param array $fallbackLanguages
55
     */
56
    public function setFallbackLanguages(array $fallbackLanguages): void
57
    {
58
        $this->fallbackLanguages = $fallbackLanguages;
59
    }
60
61
    /**
62 15
     * Add new resource to parse translations from
63
     * @param IResource $resource
64 15
     * @return Translator
65 15
     */
66
    public function addResource(IResource $resource): self
67
    {
68
        $this->resources[] = $resource;
69
        return $this;
70
    }
71
72
    /**
73
     * Provide translation
74 15
     * @param string|int $message
75
     * @param mixed ...$parameters
76
     * @return string
77
     */
78
    public function translate($message, ...$parameters): string
79 15
    {
80 15
        // translate($message, int $count, array $params, string $lang = null)
81
        // translate($message, array $params, string $lang = null)
82
83 15
        $message = (string)$message;
84
        list($count, $params, $lang) = array_values($this->parseParameters($parameters));
85
86
        // If wrong input arguments passed, return message key
87 15
        if (!is_int($count) || !is_array($params) || !is_string($lang)) {
88 15
            Arrays::invoke($this->onTranslate, $this, $message, $message, $lang, (int)strval($count), (array)$params);
89
            return $message; // @ maybe throw exception?
90
        }
91
92
        $translation = $this->getDictionary($lang)->findTranslation($message);
93
        if ($translation === null) {
94
            // Try find translation in fallback languages
95
            foreach ($this->fallbackLanguages as $fallbackLanguage) {
96
                $translation = $this->getDictionary($fallbackLanguage)->findTranslation($message);
97
                if ($translation !== null) {
98
                    break;
99
                }
100
            }
101
102
            // If translation not found in base either fallback languages return message key
103 15
            if ($translation === null) {
104 15
                Arrays::invoke($this->onTranslate, $this, $message, $translation, $lang, $count, $params);
105
                return $message;
106 15
            }
107
        }
108
109
        $translation = $this->selectRightPluralForm($translation, $lang, $count);
110
        $translation = $this->replaceParams($translation, $params);
111
112
        Arrays::invoke($this->onTranslate, $this, $message, $translation, $lang, $count, $params);
113
        return $translation;
114
    }
115
116 15
    /**
117
     * Select right plural form based on selected language
118 15
     * @param string $translation
119 15
     * @param string $lang
120 6
     * @param int $count
121
     * @return string
122
     */
123 9
    private function selectRightPluralForm(string $translation, string $lang, int $count): string
124 9
    {
125
        $exploded = explode('|', $translation);
126
        if (count($exploded) === 1) {
127
            return $translation;
128
        }
129
        foreach ($exploded as $value) {
130
            $translationPlural = $this->findSpecialFormat($value, $count);
131
            if ($translationPlural !== null) {
132
                return $translationPlural;
133 15
            }
134
        }
135 15
        $pluralForm = PluralForm::get($count, $lang);
136 15
        return $exploded[$pluralForm] ?? $exploded[0];
137 15
    }
138
139
    private function findSpecialFormat(string $translationForm, int $count): ?string
140 15
    {
141
        preg_match('/^\{ *[\d, ]+ *\}/', $translationForm, $result);
142
        $match = reset($result);
143
        if (!empty($match)) {
144
            $translationForm = str_replace($match, '', $translationForm);
145
            $match = str_replace(['{', '}', ' '], '', trim($match));
146
            $foundCountArray = explode(',', $match);
147
            foreach ($foundCountArray as $foundCount) {
148 15
                if ($count === (int)$foundCount) {
149
                    return $translationForm;
150 15
                }
151
            }
152 9
        }
153
        preg_match('/^[\[,\]] *[+,-]? *[\d,Inf]+ *, *[+,-]? *[\d,Inf]+ *[\[,\]]/', $translationForm, $result);
154 9
        $match = reset($result);
155
        if (!empty($match)) {
156
            $startChar = substr($match, 0, 1);
157
            $endChar = substr($match, -1);
158 15
            $translationForm = str_replace($match, '', $translationForm);
159
            $range = substr($match, 1, strlen($match) - 2);
160 12
            $rangeArray = explode(',', $range);
161 12
            $fromRaw = str_replace(' ', '', $rangeArray[0]);
162 12
            $toRaw = str_replace(' ', '', $rangeArray[1]);
163
            $from = $fromRaw === '-Inf' ? PHP_INT_MIN : (int)$fromRaw;
164
            $to = ($toRaw === 'Inf' || $toRaw === '+Inf') ? PHP_INT_MAX : (int)$toRaw;
165
            if (
166 15
                ($startChar === '[' && $endChar === ']' && $from <= $count && $count <= $to) ||
167 15
                ($startChar === ']' && $endChar === ']' && $from < $count && $count <= $to) ||
168 15
                ($startChar === ']' && $endChar === '[' && $from < $count && $count < $to) ||
169
                ($startChar === '[' && $endChar === '[' && $from <= $count && $count < $to)
170
            ) {
171
                return $translationForm;
172 15
            }
173 15
        }
174 15
        return null;
175
    }
176
177
    /**
178
     * Replace parameters in translation string
179
     * @param string $translation
180
     * @param array $params
181
     * @return string
182 15
     */
183
    private function replaceParams(string $translation, array $params): string
184 15
    {
185 15
        $transParams = [];
186 15
        foreach ($params as $key => $value) {
187
            $transParams['%' . $key . '%'] = $value;
188 15
        }
189
190
        return strtr($translation, $transParams);
191
    }
192
193
    /**
194
     * Parse translation input parameters
195
     * @param array $parameters
196 15
     * @return array
197
     */
198 15
    private function parseParameters(array $parameters): array
199 12
    {
200
        if (!count($parameters)) {
201
            return [
202 15
                'count' => 1,
203 15
                'params' => ['count' => 1],
204
                'lang' => $this->getResolvedLang()
205
            ];
206
        }
207
208 15
        if (is_array($parameters[0])) {
209 15
            return [
210 15
                'count' => isset($parameters[0]['count']) ? $parameters[0]['count'] : 1,
211 15
                'params' => $parameters[0],
212 15
                'lang' => array_key_exists(1, $parameters) ? $parameters[1] : $this->getResolvedLang()
213
            ];
214
        }
215 15
216
        $params = array_key_exists(1, $parameters) ? $parameters[1] : [];
217
        if ($parameters[0] && !isset($params['count'])) {
218
            $params['count'] = $parameters[0];
219 15
        }
220
221
        return [
222
            'count' => $parameters[0] ?? 1,
223
            'params' => $params,
224
            'lang' => array_key_exists(2, $parameters) ? $parameters[2] : $this->getResolvedLang()
225
        ];
226
    }
227
228
    /**
229
     * Use resolvers to resolve language to use
230
     * @return string
231
     */
232
    private function getResolvedLang(): string
233
    {
234
        if ($this->lang === null) {
235
            $resolvedLang = $this->resolver->resolve();
236
            $this->lang = $resolvedLang !== null ? $resolvedLang : $this->defaultLang;
237
        }
238
        return $this->lang;
239
    }
240
241
    /**
242
     * Prepare and return dictionary ready to use
243
     * @param string $lang
244
     * @return Dictionary
245
     */
246
    private function getDictionary(string $lang): Dictionary
247
    {
248
        if (array_key_exists($lang, $this->dictionaries)) {
249
            return $this->dictionaries[$lang];
250
        }
251
252
        $dictionary = $this->cache->load($lang);
253
        if ($dictionary !== null) {
254
            $this->dictionaries[$lang] = $dictionary;
255
            return $this->dictionaries[$lang];
256
        }
257
258
        $this->dictionaries[$lang] = new Dictionary($lang);
259
        foreach ($this->resources as $resource) {
260
            $dictionaries = $resource->load($lang);
261
            foreach ($dictionaries as $dictionary) {
262
                if (!$dictionary instanceof Dictionary) {
263
                    throw new InvalidArgumentException(sprintf('%s expected. Resource returned %s', Dictionary::class, get_class($dictionary)));
264
                }
265
                $this->dictionaries[$lang]->extend($dictionary);
266
            }
267
        }
268
        $this->cache->store($lang, $this->dictionaries[$lang]->getRecords());
269
270
        return $this->dictionaries[$lang];
271
    }
272
273
    public function reset(): void
274
    {
275
        $this->resources = [];
276
        $this->dictionaries = [];
277
    }
278
}
279