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