Completed
Push — master ( 9b2af0...bbcc3e )
by El
03:42
created

lib/I18n.php (8 issues)

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
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
0 ignored issues
show
There is no parameter named $args. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
83
     * @return string
84
     */
85 81
    public static function _($messageId)
0 ignored issues
show
The parameter $messageId is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
86
    {
87 81
        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
0 ignored issues
show
There is no parameter named $args. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
97
     * @return string
98
     */
99 81
    public static function translate($messageId)
100
    {
101 81
        if (empty($messageId)) {
102 34
            return $messageId;
103
        }
104 81
        if (count(self::$_translations) === 0) {
105 63
            self::loadTranslations();
106
        }
107 81
        $messages = $messageId;
108 81
        if (is_array($messageId)) {
109 35
            $messageId = count($messageId) > 1 ? $messageId[1] : $messageId[0];
110
        }
111 81
        if (!array_key_exists($messageId, self::$_translations)) {
112 65
            self::$_translations[$messageId] = $messages;
113
        }
114 81
        $args = func_get_args();
115 81
        if (is_array(self::$_translations[$messageId])) {
116 44
            $number = (int) $args[1];
117 44
            $key    = self::_getPluralForm($number);
118 44
            $max    = count(self::$_translations[$messageId]) - 1;
119 44
            if ($key > $max) {
120
                $key = $max;
121
            }
122
123 44
            $args[0] = self::$_translations[$messageId][$key];
124 44
            $args[1] = $number;
125
        } else {
126 80
            $args[0] = self::$_translations[$messageId];
127
        }
128 81
        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 73
    public static function loadTranslations()
0 ignored issues
show
loadTranslations uses the super-global variable $_COOKIE which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
140
    {
141 73
        $availableLanguages = self::getAvailableLanguages();
142
143
        // check if the lang cookie was set and that language exists
144
        if (
145 73
            array_key_exists('lang', $_COOKIE) &&
146 73
            ($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 66
            $match = self::_getMatchingLanguage(
153 66
                self::getBrowserLanguages(), $availableLanguages
154
            );
155
        }
156
157
        // load translations
158 73
        self::$_language     = $match;
159 73
        self::$_translations = ($match == 'en') ? array() : json_decode(
0 ignored issues
show
Documentation Bug introduced by
It seems like $match == 'en' ? array()...atch . '.json')), true) of type * is incompatible with the declared type array of property $_translations.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
160 16
            file_get_contents(self::_getPath($match . '.json')),
161 73
            true
162
        );
163 73
    }
164
165
    /**
166
     * get list of available translations based on files found
167
     *
168
     * @access public
169
     * @static
170
     * @return array
171
     */
172 109
    public static function getAvailableLanguages()
173
    {
174 109
        if (count(self::$_availableLanguages) == 0) {
175 95
            $i18n = dir(self::_getPath());
176 95
            while (false !== ($file = $i18n->read())) {
177 95
                if (preg_match('/^([a-z]{2}).json$/', $file, $match) === 1) {
178 95
                    self::$_availableLanguages[] = $match[1];
179
                }
180
            }
181 95
            self::$_availableLanguages[] = 'en';
182
        }
183 109
        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 66
    public static function getBrowserLanguages()
0 ignored issues
show
getBrowserLanguages uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
196
    {
197 66
        $languages = array();
198 66
        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
                    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 66
        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 36
    public static function getLanguageLabels($languages = array())
244
    {
245 36
        $file = self::_getPath('languages.json');
246 36
        if (count(self::$_languageLabels) == 0 && is_readable($file)) {
247 35
            self::$_languageLabels = json_decode(file_get_contents($file), true);
0 ignored issues
show
Documentation Bug introduced by
It seems like json_decode(file_get_contents($file), true) of type * is incompatible with the declared type array of property $_languageLabels.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
248
        }
249 36
        if (count($languages) == 0) {
250
            return self::$_languageLabels;
251
        }
252 36
        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 94
    public static function setLanguageFallback($lang)
263
    {
264 94
        if (in_array($lang, self::getAvailableLanguages())) {
265 2
            self::$_languageFallback = $lang;
266
        }
267 94
    }
268
269
    /**
270
     * get language file path
271
     *
272
     * @access protected
273
     * @static
274
     * @param  string $file
275
     * @return string
276
     */
277 107
    protected static function _getPath($file = '')
278
    {
279 107
        if (strlen(self::$_path) == 0) {
280 95
            self::$_path = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'i18n';
281
        }
282 107
        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 44
    protected static function _getPluralForm($n)
296
    {
297 44
        switch (self::$_language) {
298 44
            case 'fr':
299 41
            case 'oc':
300 40
            case 'zh':
301 5
                return $n > 1 ? 1 : 0;
302 39
            case 'pl':
303 1
                return $n == 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
304 38
            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 37
            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 36
                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 66
    protected static function _getMatchingLanguage($acceptedLanguages, $availableLanguages)
326
    {
327 66
        $matches = array();
328 66
        $any     = false;
329 66
        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 66
        if (count($matches) === 0 && $any) {
354 1
            if (count($availableLanguages) > 0) {
355 1
                $matches['1.0'] = $availableLanguages;
356
            }
357
        }
358 66
        if (count($matches) === 0) {
359 57
            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