I18n::setLanguageFallback()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 1
crap 2
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.2.1
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 67
    public static function _($messageId)
86
    {
87 67
        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 67
    public static function translate($messageId)
100
    {
101 67
        if (empty($messageId)) {
102 20
            return $messageId;
103
        }
104 67
        if (count(self::$_translations) === 0) {
105 49
            self::loadTranslations();
106
        }
107 67
        $messages = $messageId;
108 67
        if (is_array($messageId)) {
109 21
            $messageId = count($messageId) > 1 ? $messageId[1] : $messageId[0];
110
        }
111 67
        if (!array_key_exists($messageId, self::$_translations)) {
112 54
            self::$_translations[$messageId] = $messages;
113
        }
114 67
        $args = func_get_args();
115 67
        if (is_array(self::$_translations[$messageId])) {
116 30
            $number = (int) $args[1];
117 30
            $key    = self::_getPluralForm($number);
118 30
            $max    = count(self::$_translations[$messageId]) - 1;
119 30
            if ($key > $max) {
120
                $key = $max;
121
            }
122
123 30
            $args[0] = self::$_translations[$messageId][$key];
124 30
            $args[1] = $number;
125
        } else {
126 66
            $args[0] = self::$_translations[$messageId];
127
        }
128 67
        return call_user_func_array('sprintf', $args);
129
    }
130
131
    /**
132
     * loads translations
133
     *
134
     * From: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
135
     *
136
     * @access public
137
     * @static
138
     */
139 59
    public static function loadTranslations()
140
    {
141 59
        $availableLanguages = self::getAvailableLanguages();
142
143
        // check if the lang cookie was set and that language exists
144
        if (
145 59
            array_key_exists('lang', $_COOKIE) &&
146 59
            ($key = array_search($_COOKIE['lang'], $availableLanguages)) !== false
147
        ) {
148 7
            $match = $availableLanguages[$key];
149
        }
150
        // find a translation file matching the browsers language preferences
151
        else {
152 52
            $match = self::_getMatchingLanguage(
153 52
                self::getBrowserLanguages(), $availableLanguages
154
            );
155
        }
156
157
        // load translations
158 59
        self::$_language     = $match;
159 59
        self::$_translations = ($match == 'en') ? array() : Json::decode(
160 59
            file_get_contents(self::_getPath($match . '.json'))
161
        );
162 59
    }
163
164
    /**
165
     * get list of available translations based on files found
166
     *
167
     * @access public
168
     * @static
169
     * @return array
170
     */
171 95
    public static function getAvailableLanguages()
172
    {
173 95
        if (count(self::$_availableLanguages) == 0) {
174 81
            $i18n = dir(self::_getPath());
175 81
            while (false !== ($file = $i18n->read())) {
176 81
                if (preg_match('/^([a-z]{2}).json$/', $file, $match) === 1) {
177 81
                    self::$_availableLanguages[] = $match[1];
178
                }
179
            }
180 81
            self::$_availableLanguages[] = 'en';
181
        }
182 95
        return self::$_availableLanguages;
183
    }
184
185
    /**
186
     * detect the clients supported languages and return them ordered by preference
187
     *
188
     * From: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
189
     *
190
     * @access public
191
     * @static
192
     * @return array
193
     */
194 52
    public static function getBrowserLanguages()
195
    {
196 52
        $languages = array();
197 52
        if (array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER)) {
198 11
            $languageRanges = explode(',', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']));
199 11
            foreach ($languageRanges as $languageRange) {
200 11
                if (preg_match(
201 11
                    '/(\*|[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})))?/',
202 11
                    trim($languageRange), $match
203
                )) {
204 11
                    if (!isset($match[2])) {
205 5
                        $match[2] = '1.0';
206
                    } else {
207 8
                        $match[2] = (string) floatval($match[2]);
208
                    }
209 11
                    if (!isset($languages[$match[2]])) {
210 11
                        $languages[$match[2]] = array();
211
                    }
212 11
                    $languages[$match[2]][] = strtolower($match[1]);
213
                }
214
            }
215 11
            krsort($languages);
216
        }
217 52
        return $languages;
218
    }
219
220
    /**
221
     * get currently loaded language
222
     *
223
     * @access public
224
     * @static
225
     * @return string
226
     */
227 2
    public static function getLanguage()
228
    {
229 2
        return self::$_language;
230
    }
231
232
    /**
233
     * get list of language labels
234
     *
235
     * Only for given language codes, otherwise all labels.
236
     *
237
     * @access public
238
     * @static
239
     * @param  array $languages
240
     * @return array
241
     */
242 23
    public static function getLanguageLabels($languages = array())
243
    {
244 23
        $file = self::_getPath('languages.json');
245 23
        if (count(self::$_languageLabels) == 0 && is_readable($file)) {
246 21
            self::$_languageLabels = Json::decode(file_get_contents($file));
247
        }
248 23
        if (count($languages) == 0) {
249 1
            return self::$_languageLabels;
250
        }
251 22
        return array_intersect_key(self::$_languageLabels, array_flip($languages));
252
    }
253
254
    /**
255
     * set the default language
256
     *
257
     * @access public
258
     * @static
259
     * @param  string $lang
260
     */
261 80
    public static function setLanguageFallback($lang)
262
    {
263 80
        if (in_array($lang, self::getAvailableLanguages())) {
264 2
            self::$_languageFallback = $lang;
265
        }
266 80
    }
267
268
    /**
269
     * get language file path
270
     *
271
     * @access protected
272
     * @static
273
     * @param  string $file
274
     * @return string
275
     */
276 94
    protected static function _getPath($file = '')
277
    {
278 94
        if (strlen(self::$_path) == 0) {
279 81
            self::$_path = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'i18n';
280
        }
281 94
        return self::$_path . (strlen($file) ? DIRECTORY_SEPARATOR . $file : '');
282
    }
283
284
    /**
285
     * determines the plural form to use based on current language and given number
286
     *
287
     * From: http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html
288
     *
289
     * @access protected
290
     * @static
291
     * @param  int $n
292
     * @return int
293
     */
294 30
    protected static function _getPluralForm($n)
295
    {
296 30
        switch (self::$_language) {
297 30
            case 'fr':
298 27
            case 'oc':
299 26
            case 'zh':
300 5
                return $n > 1 ? 1 : 0;
301 25
            case 'pl':
302 1
                return $n == 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
303 24
            case 'ru':
304 1
                return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
305 23
            case 'sl':
306 1
                return $n % 100 == 1 ? 1 : ($n % 100 == 2 ? 2 : ($n % 100 == 3 || $n % 100 == 4 ? 3 : 0));
307
            // de, en, es, hu, it, nl, no, pt
308
            default:
309 22
                return $n != 1 ? 1 : 0;
310
        }
311
    }
312
313
    /**
314
     * compares two language preference arrays and returns the preferred match
315
     *
316
     * From: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
317
     *
318
     * @access protected
319
     * @static
320
     * @param  array $acceptedLanguages
321
     * @param  array $availableLanguages
322
     * @return string
323
     */
324 52
    protected static function _getMatchingLanguage($acceptedLanguages, $availableLanguages)
325
    {
326 52
        $matches = array();
327 52
        $any     = false;
328 52
        foreach ($acceptedLanguages as $acceptedQuality => $acceptedValues) {
329 11
            $acceptedQuality = floatval($acceptedQuality);
330 11
            if ($acceptedQuality === 0.0) {
331 2
                continue;
332
            }
333 11
            foreach ($availableLanguages as $availableValue) {
334 11
                $availableQuality = 1.0;
335 11
                foreach ($acceptedValues as $acceptedValue) {
336 11
                    if ($acceptedValue === '*') {
337 1
                        $any = true;
338
                    }
339 11
                    $matchingGrade = self::_matchLanguage($acceptedValue, $availableValue);
340 11
                    if ($matchingGrade > 0) {
341 8
                        $q = (string) ($acceptedQuality * $availableQuality * $matchingGrade);
342 8
                        if (!isset($matches[$q])) {
343 8
                            $matches[$q] = array();
344
                        }
345 8
                        if (!in_array($availableValue, $matches[$q])) {
346 11
                            $matches[$q][] = $availableValue;
347
                        }
348
                    }
349
                }
350
            }
351
        }
352 52
        if (count($matches) === 0 && $any) {
353 1
            if (count($availableLanguages) > 0) {
354 1
                $matches['1.0'] = $availableLanguages;
355
            }
356
        }
357 52
        if (count($matches) === 0) {
358 43
            return self::$_languageFallback;
359
        }
360 9
        krsort($matches);
361 9
        $topmatches = current($matches);
362 9
        return current($topmatches);
363
    }
364
365
    /**
366
     * compare two language IDs and return the degree they match
367
     *
368
     * From: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
369
     *
370
     * @access protected
371
     * @static
372
     * @param  string $a
373
     * @param  string $b
374
     * @return float
375
     */
376 11
    protected static function _matchLanguage($a, $b)
377
    {
378 11
        $a = explode('-', $a);
379 11
        $b = explode('-', $b);
380 11
        for ($i = 0, $n = min(count($a), count($b)); $i < $n; ++$i) {
381 11
            if ($a[$i] !== $b[$i]) {
382 11
                break;
383
            }
384
        }
385 11
        return $i === 0 ? 0 : (float) $i / count($a);
386
    }
387
}
388