Passed
Push — master ( 81ac23...a5d5f6 )
by El
03:02
created

lib/I18n.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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.1.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 83
    public static function _($messageId)
86
    {
87 83
        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 83
    public static function translate($messageId)
100
    {
101 83
        if (empty($messageId)) {
102 36
            return $messageId;
103
        }
104 83
        if (count(self::$_translations) === 0) {
105 65
            self::loadTranslations();
106
        }
107 83
        $messages = $messageId;
108 83
        if (is_array($messageId)) {
109 37
            $messageId = count($messageId) > 1 ? $messageId[1] : $messageId[0];
110
        }
111 83
        if (!array_key_exists($messageId, self::$_translations)) {
112 65
            self::$_translations[$messageId] = $messages;
113
        }
114 83
        $args = func_get_args();
115 83
        if (is_array(self::$_translations[$messageId])) {
116 46
            $number = (int) $args[1];
117 46
            $key    = self::_getPluralForm($number);
118 46
            $max    = count(self::$_translations[$messageId]) - 1;
119 46
            if ($key > $max) {
120
                $key = $max;
121
            }
122
123 46
            $args[0] = self::$_translations[$messageId][$key];
124 46
            $args[1] = $number;
125
        } else {
126 82
            $args[0] = self::$_translations[$messageId];
127
        }
128 83
        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 75
    public static function loadTranslations()
140
    {
141 75
        $availableLanguages = self::getAvailableLanguages();
142
143
        // check if the lang cookie was set and that language exists
144
        if (
145 75
            array_key_exists('lang', $_COOKIE) &&
146 75
            ($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 68
            $match = self::_getMatchingLanguage(
153 68
                self::getBrowserLanguages(), $availableLanguages
154
            );
155
        }
156
157
        // load translations
158 75
        self::$_language     = $match;
159 75
        self::$_translations = ($match == 'en') ? array() : json_decode(
160 16
            file_get_contents(self::_getPath($match . '.json')),
161 75
            true
162
        );
163 75
    }
164
165
    /**
166
     * get list of available translations based on files found
167
     *
168
     * @access public
169
     * @static
170
     * @return array
171
     */
172 111
    public static function getAvailableLanguages()
173
    {
174 111
        if (count(self::$_availableLanguages) == 0) {
175 97
            $i18n = dir(self::_getPath());
176 97
            while (false !== ($file = $i18n->read())) {
177 97
                if (preg_match('/^([a-z]{2}).json$/', $file, $match) === 1) {
178 97
                    self::$_availableLanguages[] = $match[1];
179
                }
180
            }
181 97
            self::$_availableLanguages[] = 'en';
182
        }
183 111
        return self::$_availableLanguages;
184
    }
185
186
    /**
187
     * detect the clients supported languages and return them ordered by preference
188
     *
189
     * From: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
190
     *
191
     * @access public
192
     * @static
193
     * @return array
194
     */
195 68
    public static function getBrowserLanguages()
196
    {
197 68
        $languages = array();
198 68
        if (array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER)) {
199 11
            $languageRanges = explode(',', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']));
200 11 View Code Duplication
            foreach ($languageRanges as $languageRange) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
201 11
                if (preg_match(
202 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})))?/',
203 11
                    trim($languageRange), $match
204
                )) {
205 11
                    if (!isset($match[2])) {
206 5
                        $match[2] = '1.0';
207
                    } else {
208 8
                        $match[2] = (string) floatval($match[2]);
209
                    }
210 11
                    if (!isset($languages[$match[2]])) {
211 11
                        $languages[$match[2]] = array();
212
                    }
213 11
                    $languages[$match[2]][] = strtolower($match[1]);
214
                }
215
            }
216 11
            krsort($languages);
217
        }
218 68
        return $languages;
219
    }
220
221
    /**
222
     * get currently loaded language
223
     *
224
     * @access public
225
     * @static
226
     * @return string
227
     */
228 2
    public static function getLanguage()
229
    {
230 2
        return self::$_language;
231
    }
232
233
    /**
234
     * get list of language labels
235
     *
236
     * Only for given language codes, otherwise all labels.
237
     *
238
     * @access public
239
     * @static
240
     * @param  array $languages
241
     * @return array
242
     */
243 38
    public static function getLanguageLabels($languages = array())
244
    {
245 38
        $file = self::_getPath('languages.json');
246 38
        if (count(self::$_languageLabels) == 0 && is_readable($file)) {
247 37
            self::$_languageLabels = json_decode(file_get_contents($file), true);
248
        }
249 38
        if (count($languages) == 0) {
250
            return self::$_languageLabels;
251
        }
252 38
        return array_intersect_key(self::$_languageLabels, array_flip($languages));
253
    }
254
255
    /**
256
     * set the default language
257
     *
258
     * @access public
259
     * @static
260
     * @param  string $lang
261
     */
262 96
    public static function setLanguageFallback($lang)
263
    {
264 96
        if (in_array($lang, self::getAvailableLanguages())) {
265 2
            self::$_languageFallback = $lang;
266
        }
267 96
    }
268
269
    /**
270
     * get language file path
271
     *
272
     * @access protected
273
     * @static
274
     * @param  string $file
275
     * @return string
276
     */
277 109
    protected static function _getPath($file = '')
278
    {
279 109
        if (strlen(self::$_path) == 0) {
280 97
            self::$_path = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'i18n';
281
        }
282 109
        return self::$_path . (strlen($file) ? DIRECTORY_SEPARATOR . $file : '');
283
    }
284
285
    /**
286
     * determines the plural form to use based on current language and given number
287
     *
288
     * From: http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html
289
     *
290
     * @access protected
291
     * @static
292
     * @param  int $n
293
     * @return int
294
     */
295 46
    protected static function _getPluralForm($n)
296
    {
297 46
        switch (self::$_language) {
298 46
            case 'fr':
299 43
            case 'oc':
300 42
            case 'zh':
301 5
                return $n > 1 ? 1 : 0;
302 41
            case 'pl':
303 1
                return $n == 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
304 40
            case 'ru':
305 1
                return $n % 10 == 1 && $n % 100 != 11 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
306 39
            case 'sl':
307 1
                return $n % 100 == 1 ? 1 : ($n % 100 == 2 ? 2 : ($n % 100 == 3 || $n % 100 == 4 ? 3 : 0));
308
            // de, en, es, it, no, pt
309
            default:
310 38
                return $n != 1 ? 1 : 0;
311
        }
312
    }
313
314
    /**
315
     * compares two language preference arrays and returns the preferred match
316
     *
317
     * From: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
318
     *
319
     * @access protected
320
     * @static
321
     * @param  array $acceptedLanguages
322
     * @param  array $availableLanguages
323
     * @return string
324
     */
325 68
    protected static function _getMatchingLanguage($acceptedLanguages, $availableLanguages)
326
    {
327 68
        $matches = array();
328 68
        $any     = false;
329 68
        foreach ($acceptedLanguages as $acceptedQuality => $acceptedValues) {
330 11
            $acceptedQuality = floatval($acceptedQuality);
331 11
            if ($acceptedQuality === 0.0) {
332
                continue;
333
            }
334 11
            foreach ($availableLanguages as $availableValue) {
335 11
                $availableQuality = 1.0;
336 11
                foreach ($acceptedValues as $acceptedValue) {
337 11
                    if ($acceptedValue === '*') {
338 1
                        $any = true;
339
                    }
340 11
                    $matchingGrade = self::_matchLanguage($acceptedValue, $availableValue);
341 11
                    if ($matchingGrade > 0) {
342 8
                        $q = (string) ($acceptedQuality * $availableQuality * $matchingGrade);
343 8
                        if (!isset($matches[$q])) {
344 8
                            $matches[$q] = array();
345
                        }
346 8
                        if (!in_array($availableValue, $matches[$q])) {
347 11
                            $matches[$q][] = $availableValue;
348
                        }
349
                    }
350
                }
351
            }
352
        }
353 68
        if (count($matches) === 0 && $any) {
354 1
            if (count($availableLanguages) > 0) {
355 1
                $matches['1.0'] = $availableLanguages;
356
            }
357
        }
358 68
        if (count($matches) === 0) {
359 59
            return self::$_languageFallback;
360
        }
361 9
        krsort($matches);
362 9
        $topmatches = current($matches);
363 9
        return current($topmatches);
364
    }
365
366
    /**
367
     * compare two language IDs and return the degree they match
368
     *
369
     * From: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
370
     *
371
     * @access protected
372
     * @static
373
     * @param  string $a
374
     * @param  string $b
375
     * @return float
376
     */
377 11
    protected static function _matchLanguage($a, $b)
378
    {
379 11
        $a = explode('-', $a);
380 11
        $b = explode('-', $b);
381 11
        for ($i = 0, $n = min(count($a), count($b)); $i < $n; ++$i) {
382 11
            if ($a[$i] !== $b[$i]) {
383 11
                break;
384
            }
385
        }
386 11
        return $i === 0 ? 0 : (float) $i / count($a);
387
    }
388
}
389