I18n::translate()   C
last analyzed

Complexity

Conditions 13
Paths 181

Size

Total Lines 42
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 13.0069

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 29
c 1
b 0
f 0
nc 181
nop 2
dl 0
loc 42
ccs 28
cts 29
cp 0.9655
crap 13.0069
rs 5.9416

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