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