Issues (4)

src/Translator.php (1 issue)

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\Record\NullRecord;
10
use Efabrica\Translatte\Record\RecordInterface;
11
use Efabrica\Translatte\Resolver\IResolver;
12
use Efabrica\Translatte\Resolver\StaticResolver;
13
use Efabrica\Translatte\Resource\IResource;
14
use InvalidArgumentException;
15
use Nette\Localization\ITranslator;
16
use Nette\Utils\Arrays;
17
18
class Translator implements ITranslator
0 ignored issues
show
Deprecated Code introduced by
The interface Nette\Localization\ITranslator has been deprecated: use Nette\Localization\Translator ( Ignorable by Annotation )

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

18
class Translator implements /** @scrutinizer ignore-deprecated */ ITranslator

This interface has been deprecated. The supplier of the interface has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the interface will be removed and what other interface to use instead.

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