I18n::_getPluralForm()   F
last analyzed

Complexity

Conditions 60
Paths 272

Size

Total Lines 35
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 73.7197

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 60
eloc 32
c 1
b 0
f 0
nc 272
nop 1
dl 0
loc 35
ccs 27
cts 32
cp 0.8438
crap 73.7197
rs 2.4333

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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