Passed
Push — master ( 3f508b...939a62 )
by El
05:26
created

I18n::translate()   C

Complexity

Conditions 8
Paths 37

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 8.0069

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 20
cts 21
cp 0.9524
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 21
nc 37
nop 1
crap 8.0069
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 71
            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
            foreach ($languageRanges as $languageRange) {
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