I18n::getLanguage()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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