I18n   F
last analyzed

Complexity

Total Complexity 115

Size/Duplication

Total Lines 420
Duplicated Lines 0 %

Test Coverage

Coverage 96.08%

Importance

Changes 4
Bugs 2 Features 1
Metric Value
wmc 115
eloc 148
c 4
b 2
f 1
dl 0
loc 420
ccs 147
cts 153
cp 0.9608
rs 2

14 Methods

Rating   Name   Duplication   Size   Complexity  
A _matchLanguage() 0 10 4
C _getMatchingLanguage() 0 39 13
A _() 0 3 1
A loadTranslations() 0 22 4
A getAvailableLanguages() 0 15 4
F _getPluralForm() 0 35 60
A _getPath() 0 6 3
A setLanguageFallback() 0 4 2
A getLanguageLabels() 0 10 4
A encode() 0 3 1
B translate() 0 37 11
A getBrowserLanguages() 0 24 6
A getLanguage() 0 3 1
A isRtl() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like I18n 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 I18n, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * PrivateBin
4
 *
5
 * a zero-knowledge paste bin
6
 *
7
 * @link      https://github.com/PrivateBin/PrivateBin
8
 * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
9
 * @license   https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
10
 * @version   1.7.1
11
 */
12
13
namespace PrivateBin;
14
15
use AppendIterator;
16
use GlobIterator;
17
18
/**
19
 * I18n
20
 *
21
 * provides internationalization tools like translation, browser language detection, etc.
22
 */
23
class I18n
24
{
25
    /**
26
     * language
27
     *
28
     * @access protected
29
     * @static
30
     * @var    string
31
     */
32
    protected static $_language = 'en';
33
34
    /**
35
     * language fallback
36
     *
37
     * @access protected
38
     * @static
39
     * @var    string
40
     */
41
    protected static $_languageFallback = 'en';
42
43
    /**
44
     * language labels
45
     *
46
     * @access protected
47
     * @static
48
     * @var    array
49
     */
50
    protected static $_languageLabels = array();
51
52
    /**
53
     * available languages
54
     *
55
     * @access protected
56
     * @static
57
     * @var    array
58
     */
59
    protected static $_availableLanguages = array();
60
61
    /**
62
     * path to language files
63
     *
64
     * @access protected
65
     * @static
66
     * @var    string
67
     */
68
    protected static $_path = '';
69
70
    /**
71
     * translation cache
72
     *
73
     * @access protected
74
     * @static
75
     * @var    array
76
     */
77
    protected static $_translations = array();
78
79
    /**
80
     * translate a string, alias for translate()
81
     *
82
     * @access public
83
     * @static
84
     * @param  string|array $messageId
85
     * @param  mixed $args one or multiple parameters injected into placeholders
86
     * @return string
87
     */
88 105
    public static function _($messageId, ...$args)
89
    {
90 105
        return forward_static_call_array('PrivateBin\I18n::translate', func_get_args());
91
    }
92
93
    /**
94
     * translate a string
95
     *
96
     * @access public
97
     * @static
98
     * @param  string|array $messageId
99
     * @param  mixed $args one or multiple parameters injected into placeholders
100
     * @return string
101
     */
102 105
    public static function translate($messageId, ...$args)
103
    {
104 105
        if (empty($messageId)) {
105 33
            return $messageId;
106
        }
107 105
        if (empty(self::$_translations)) {
108 83
            self::loadTranslations();
109
        }
110 105
        $messages = $messageId;
111 105
        if (is_array($messageId)) {
112 35
            $messageId = count($messageId) > 1 ? $messageId[1] : $messageId[0];
113
        }
114 105
        if (!array_key_exists($messageId, self::$_translations)) {
115 90
            self::$_translations[$messageId] = $messages;
116
        }
117 105
        array_unshift($args, $messageId);
118 105
        if (is_array(self::$_translations[$messageId])) {
119 45
            $number = (int) $args[1];
120 45
            $key    = self::_getPluralForm($number);
121 45
            $max    = count(self::$_translations[$messageId]) - 1;
122 45
            if ($key > $max) {
123
                $key = $max;
124
            }
125
126 45
            $args[0] = self::$_translations[$messageId][$key];
127 45
            $args[1] = $number;
128
        } else {
129 94
            $args[0] = self::$_translations[$messageId];
130
        }
131
        // encode any non-integer arguments and the message ID, if it doesn't contain a link
132 105
        $argsCount = count($args);
133 105
        for ($i = 0; $i < $argsCount; ++$i) {
134 105
            if ($i > 0 ? !is_int($args[$i]) : strpos($args[0], '<a') === false) {
135 105
                $args[$i] = self::encode($args[$i]);
136
            }
137
        }
138 105
        return call_user_func_array('sprintf', $args);
139
    }
140
141
    /**
142
     * encode HTML entities for output into an HTML5 document
143
     *
144
     * @access public
145
     * @static
146
     * @param  string $string
147
     * @return string
148
     */
149 105
    public static function encode($string)
150
    {
151 105
        return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5 | ENT_DISALLOWED, 'UTF-8', false);
152
    }
153
154
    /**
155
     * loads translations
156
     *
157
     * From: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
158
     *
159
     * @access public
160
     * @static
161
     */
162 94
    public static function loadTranslations()
163
    {
164 94
        $availableLanguages = self::getAvailableLanguages();
165
166
        // check if the lang cookie was set and that language exists
167
        if (
168 94
            array_key_exists('lang', $_COOKIE) &&
169 94
            ($key = array_search($_COOKIE['lang'], $availableLanguages)) !== false
170
        ) {
171 10
            $match = $availableLanguages[$key];
172
        }
173
        // find a translation file matching the browsers language preferences
174
        else {
175 84
            $match = self::_getMatchingLanguage(
176 84
                self::getBrowserLanguages(), $availableLanguages
177 84
            );
178
        }
179
180
        // load translations
181 94
        self::$_language     = $match;
182 94
        self::$_translations = ($match == 'en') ? array() : Json::decode(
183 94
            file_get_contents(self::_getPath($match . '.json'))
184 94
        );
185
    }
186
187
    /**
188
     * get list of available translations based on files found
189
     *
190
     * @access public
191
     * @static
192
     * @return array
193
     */
194 141
    public static function getAvailableLanguages()
195
    {
196 141
        if (count(self::$_availableLanguages) == 0) {
197 126
            self::$_availableLanguages[] = 'en'; // en.json is not part of the release archive
198 126
            $languageIterator            = new AppendIterator();
199 126
            $languageIterator->append(new GlobIterator(self::_getPath('??.json')));
200 126
            $languageIterator->append(new GlobIterator(self::_getPath('???.json'))); // for jbo
201 126
            foreach ($languageIterator as $file) {
202 126
                $language = $file->getBasename('.json');
203 126
                if ($language != 'en') {
204 126
                    self::$_availableLanguages[] = $language;
205
                }
206
            }
207
        }
208 141
        return self::$_availableLanguages;
209
    }
210
211
    /**
212
     * detect the clients supported languages and return them ordered by preference
213
     *
214
     * From: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
215
     *
216
     * @access public
217
     * @static
218
     * @return array
219
     */
220 84
    public static function getBrowserLanguages()
221
    {
222 84
        $languages = array();
223 84
        if (array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER)) {
224 13
            $languageRanges = explode(',', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']));
225 13
            foreach ($languageRanges as $languageRange) {
226 13
                if (preg_match(
227 13
                    '/(\*|[a-zA-Z0-9]{1,8}(?:-[a-zA-Z0-9]{1,8})*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?/',
228 13
                    trim($languageRange), $match
229 13
                )) {
230 13
                    if (!isset($match[2])) {
231 6
                        $match[2] = '1.0';
232
                    } else {
233 9
                        $match[2] = (string) floatval($match[2]);
234
                    }
235 13
                    if (!isset($languages[$match[2]])) {
236 13
                        $languages[$match[2]] = array();
237
                    }
238 13
                    $languages[$match[2]][] = strtolower($match[1]);
239
                }
240
            }
241 13
            krsort($languages);
242
        }
243 84
        return $languages;
244
    }
245
246
    /**
247
     * get currently loaded language
248
     *
249
     * @access public
250
     * @static
251
     * @return string
252
     */
253 47
    public static function getLanguage()
254
    {
255 47
        return self::$_language;
256
    }
257
258
    /**
259
     * get list of language labels
260
     *
261
     * Only for given language codes, otherwise all labels.
262
     *
263
     * @access public
264
     * @static
265
     * @param  array $languages
266
     * @return array
267
     */
268 33
    public static function getLanguageLabels($languages = array())
269
    {
270 33
        $file = self::_getPath('languages.json');
271 33
        if (count(self::$_languageLabels) == 0 && is_readable($file)) {
272 31
            self::$_languageLabels = Json::decode(file_get_contents($file));
273
        }
274 33
        if (count($languages) == 0) {
275 1
            return self::$_languageLabels;
276
        }
277 32
        return array_intersect_key(self::$_languageLabels, array_flip($languages));
278
    }
279
280
    /**
281
     * determines if the current language is written right-to-left (RTL)
282
     *
283
     * @access public
284
     * @static
285
     * @return bool
286
     */
287 36
    public static function isRtl()
288
    {
289 36
        return in_array(self::$_language, array('ar', 'he'));
290
    }
291
292
    /**
293
     * set the default language
294
     *
295
     * @access public
296
     * @static
297
     * @param  string $lang
298
     */
299 123
    public static function setLanguageFallback($lang)
300
    {
301 123
        if (in_array($lang, self::getAvailableLanguages())) {
302 3
            self::$_languageFallback = $lang;
303
        }
304
    }
305
306
    /**
307
     * get language file path
308
     *
309
     * @access protected
310
     * @static
311
     * @param  string $file
312
     * @return string
313
     */
314 138
    protected static function _getPath($file = '')
315
    {
316 138
        if (empty(self::$_path)) {
317 125
            self::$_path = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'i18n';
318
        }
319 138
        return self::$_path . (empty($file) ? '' : DIRECTORY_SEPARATOR . $file);
320
    }
321
322
    /**
323
     * determines the plural form to use based on current language and given number
324
     *
325
     * From: https://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
326
     *
327
     * @access protected
328
     * @static
329
     * @param  int $n
330
     * @return int
331
     */
332 45
    protected static function _getPluralForm($n)
333
    {
334 45
        switch (self::$_language) {
335 45
            case 'ar':
336
                return $n === 0 ? 0 : ($n === 1 ? 1 : ($n === 2 ? 2 : ($n % 100 >= 3 && $n % 100 <= 10 ? 3 : ($n % 100 >= 11 ? 4 : 5))));
337 45
            case 'cs':
338 44
            case 'sk':
339 1
                return $n === 1 ? 0 : ($n >= 2 && $n <= 4 ? 1 : 2);
340 44
            case 'co':
341 44
            case 'fr':
342 40
            case 'oc':
343 39
            case 'tr':
344 39
            case 'zh':
345 6
                return $n > 1 ? 1 : 0;
346 38
            case 'he':
347
                return $n === 1 ? 0 : ($n === 2 ? 1 : (($n < 0 || $n > 10) && ($n % 10 === 0) ? 2 : 3));
348 38
            case 'id':
349 38
            case 'ja':
350 38
            case 'jbo':
351 38
            case 'th':
352
                return 0;
353 38
            case 'lt':
354
                return $n % 10 === 1 && $n % 100 !== 11 ? 0 : (($n % 10 >= 2 && $n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
355 38
            case 'pl':
356 1
                return $n === 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
357 37
            case 'ro':
358
                return $n === 1 ? 0 : (($n === 0 || ($n % 100 > 0 && $n % 100 < 20)) ? 1 : 2);
359 37
            case 'ru':
360 36
            case 'uk':
361 1
                return $n % 10 === 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
362 36
            case 'sl':
363 1
                return $n % 100 === 1 ? 1 : ($n % 100 === 2 ? 2 : ($n % 100 === 3 || $n % 100 === 4 ? 3 : 0));
364
            default:
365
                // bg, ca, de, el, en, es, et, fi, hu, it, nl, no, pt
366 35
                return $n !== 1 ? 1 : 0;
367
        }
368
    }
369
370
    /**
371
     * compares two language preference arrays and returns the preferred match
372
     *
373
     * From: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
374
     *
375
     * @access protected
376
     * @static
377
     * @param  array $acceptedLanguages
378
     * @param  array $availableLanguages
379
     * @return string
380
     */
381 84
    protected static function _getMatchingLanguage($acceptedLanguages, $availableLanguages)
382
    {
383 84
        $matches = array();
384 84
        $any     = false;
385 84
        foreach ($acceptedLanguages as $acceptedQuality => $acceptedValues) {
386 13
            $acceptedQuality = floatval($acceptedQuality);
387 13
            if ($acceptedQuality === 0.0) {
388 2
                continue;
389
            }
390 13
            foreach ($availableLanguages as $availableValue) {
391 13
                $availableQuality = 1.0;
392 13
                foreach ($acceptedValues as $acceptedValue) {
393 13
                    if ($acceptedValue === '*') {
394 1
                        $any = true;
395
                    }
396 13
                    $matchingGrade = self::_matchLanguage($acceptedValue, $availableValue);
397 13
                    if ($matchingGrade > 0) {
398 9
                        $q = (string) ($acceptedQuality * $availableQuality * $matchingGrade);
399 9
                        if (!isset($matches[$q])) {
400 9
                            $matches[$q] = array();
401
                        }
402 9
                        if (!in_array($availableValue, $matches[$q])) {
403 9
                            $matches[$q][] = $availableValue;
404
                        }
405
                    }
406
                }
407
            }
408
        }
409 84
        if (count($matches) === 0 && $any) {
410 1
            if (count($availableLanguages) > 0) {
411 1
                $matches['1.0'] = $availableLanguages;
412
            }
413
        }
414 84
        if (count($matches) === 0) {
415 74
            return self::$_languageFallback;
416
        }
417 10
        krsort($matches);
418 10
        $topmatches = current($matches);
419 10
        return current($topmatches);
420
    }
421
422
    /**
423
     * compare two language IDs and return the degree they match
424
     *
425
     * From: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
426
     *
427
     * @access protected
428
     * @static
429
     * @param  string $a
430
     * @param  string $b
431
     * @return float
432
     */
433 13
    protected static function _matchLanguage($a, $b)
434
    {
435 13
        $a = explode('-', $a);
436 13
        $b = explode('-', $b);
437 13
        for ($i = 0, $n = min(count($a), count($b)); $i < $n; ++$i) {
438 13
            if ($a[$i] !== $b[$i]) {
439 13
                break;
440
            }
441
        }
442 13
        return $i === 0 ? 0 : (float) $i / count($a);
443
    }
444
}
445