Passed
Pull Request — master (#9)
by
unknown
15:05
created

Translator   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 245
Duplicated Lines 0 %

Test Coverage

Coverage 81.94%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 44
eloc 96
c 5
b 0
f 0
dl 0
loc 245
ccs 59
cts 72
cp 0.8194
rs 8.8798

10 Methods

Rating   Name   Duplication   Size   Complexity  
A setFallbackLanguages() 0 3 1
A addResource() 0 4 1
A __construct() 0 8 3
A replaceParams() 0 8 2
A getDictionary() 0 25 6
B findSpecialFormat() 0 20 7
A selectRightPluralForm() 0 14 5
B translate() 0 36 8
A getResolvedLang() 0 7 3
B parseParameters() 0 27 8

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
    /** @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 array */
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) {
132
                return $translationPlural;
133 15
            }
134
        }
135 15
        $pluralForm = PluralForm::get($count, $lang);
136 15
        return isset($exploded[$pluralForm]) ? $exploded[$pluralForm] : $exploded[0];
137 15
    }
138
139
    /**
140 15
     * example: '{0} používateľov|{1}používateľ|[2,4] používatelia|[5,Inf] používateľov';
141
     * @param string $translationForm
142
     * @param int $count
143
     * @return string|null
144
     */
145
    private function findSpecialFormat(string $translationForm, int $count): ?string
146
    {
147
        $foundCount = strtok($translationForm, '{}');
148 15
        if ($foundCount !== $translationForm) {
149
            $translationForm = str_replace('{' . $foundCount . '}', '', $translationForm);
150 15
            if ($foundCount === $count) {
151
                return $translationForm;
152 9
            }
153
        }
154 9
        $foundCount = strtok($translationForm, '[]');
155
        if ($foundCount !== $translationForm) {
156
            $translationForm = str_replace('[' . $foundCount . ']', '', $translationForm);
157
            $foundCountArray = explode(',', $foundCount);
158 15
            $from = (int)$foundCountArray[0];
159
            $to = $foundCountArray[1] === 'Inf' ? PHP_INT_MAX : (int)$foundCountArray[1];
160 12
            if ($from <= $count && $count <= $to) {
161 12
                return $translationForm;
162 12
            }
163
        }
164
        return null;
165
    }
166 15
167 15
    /**
168 15
     * Replace parameters in translation string
169
     * @param string $translation
170
     * @param array $params
171
     * @return string
172 15
     */
173 15
    private function replaceParams(string $translation, array $params): string
174 15
    {
175
        $transParams = [];
176
        foreach ($params as $key => $value) {
177
            $transParams['%' . $key . '%'] = $value;
178
        }
179
180
        return strtr($translation, $transParams);
181
    }
182 15
183
    /**
184 15
     * Parse translation input parameters
185 15
     * @param array $parameters
186 15
     * @return array
187
     */
188 15
    private function parseParameters(array $parameters): array
189
    {
190
        if (!count($parameters)) {
191
            return [
192
                'count' => 1,
193
                'params' => ['count' => 1],
194
                'lang' => $this->getResolvedLang()
195
            ];
196 15
        }
197
198 15
        if (is_array($parameters[0])) {
199 12
            return [
200
                'count' => isset($parameters[0]['count']) ? $parameters[0]['count'] : 1,
201
                'params' => $parameters[0],
202 15
                'lang' => array_key_exists(1, $parameters) ? $parameters[1] : $this->getResolvedLang()
203 15
            ];
204
        }
205
206
        $params = array_key_exists(1, $parameters) ? $parameters[1] : [];
207
        if (!isset($params['count'])) {
208 15
            $params['count'] = $parameters[0];
209 15
        }
210 15
211 15
        return [
212 15
            'count' => $parameters[0],
213
            'params' => $params,
214
            'lang' => array_key_exists(2, $parameters) ? $parameters[2] : $this->getResolvedLang()
215 15
        ];
216
    }
217
218
    /**
219 15
     * Use resolvers to resolve language to use
220
     * @return string
221
     */
222
    private function getResolvedLang(): string
223
    {
224
        if ($this->lang === null) {
225
            $resolvedLang = $this->resolver->resolve();
226
            $this->lang = $resolvedLang !== null ? $resolvedLang : $this->defaultLang;
227
        }
228
        return $this->lang;
229
    }
230
231
    /**
232
     * Prepare and return dictionary ready to use
233
     * @param string $lang
234
     * @return Dictionary
235
     */
236
    private function getDictionary(string $lang): Dictionary
237
    {
238
        if (array_key_exists($lang, $this->dictionaries)) {
239
            return $this->dictionaries[$lang];
240
        }
241
242
        $dictionary = $this->cache->load($lang);
243
        if ($dictionary !== null) {
244
            $this->dictionaries[$lang] = $dictionary;
245
            return $this->dictionaries[$lang];
246
        }
247
248
        $this->dictionaries[$lang] = new Dictionary($lang);
249
        foreach ($this->resources as $resource) {
250
            $dictionaries = $resource->load($lang);
251
            foreach ($dictionaries as $dictionary) {
252
                $this->cache->store($lang, $dictionary->getRecords());
253
                if (!$dictionary instanceof Dictionary) {
254
                    throw new InvalidArgumentException(sprintf('%s expected. Resource returned %s', Dictionary::class, get_class($dictionary)));
255
                }
256
                $this->dictionaries[$lang]->extend($dictionary);
257
            }
258
        }
259
260
        return $this->dictionaries[$lang];
261
    }
262
}
263