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