Translator   F
last analyzed

Complexity

Total Complexity 64

Size/Duplication

Total Lines 277
Duplicated Lines 0 %

Test Coverage

Coverage 81.94%

Importance

Changes 13
Bugs 1 Features 0
Metric Value
wmc 64
eloc 119
c 13
b 1
f 0
dl 0
loc 277
ccs 59
cts 72
cp 0.8194
rs 3.28

13 Methods

Rating   Name   Duplication   Size   Complexity  
A setFallbackLanguages() 0 3 1
A addResource() 0 4 1
A getDictionary() 0 25 6
A replaceParams() 0 8 2
A reset() 0 4 1
A fixBackEscapedDelimiter() 0 3 1
D findSpecialFormat() 0 36 24
A __construct() 0 8 3
B translate() 0 38 8
A fixEscapedDelimiter() 0 3 1
A selectRightPluralForm() 0 14 4
A getResolvedLang() 0 7 3
B parseParameters() 0 27 9

How to fix   Complexity   

Complex Class

Complex classes like Translator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Translator, and based on these observations, apply Extract Interface, too.

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