i18n.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. <?php
  2. /**
  3. * ZeroBin
  4. *
  5. * a zero-knowledge paste bin
  6. *
  7. * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
  8. * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
  9. * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
  10. * @version 0.20
  11. */
  12. /**
  13. * i18n
  14. *
  15. * provides internationalization tools like translation, browser language detection, etc.
  16. */
  17. class i18n
  18. {
  19. /**
  20. * language
  21. *
  22. * @access protected
  23. * @static
  24. * @var string
  25. */
  26. protected static $_language = 'en';
  27. /**
  28. * translation cache
  29. *
  30. * @access protected
  31. * @static
  32. * @var array
  33. */
  34. protected static $_translations = array();
  35. /**
  36. * translate a string, alias for translate()
  37. *
  38. * @access public
  39. * @static
  40. * @param string $messageId
  41. * @param mixed $args one or multiple parameters injected into placeholders
  42. * @return string
  43. */
  44. public static function _($messageId)
  45. {
  46. return call_user_func_array(array('i18n', 'translate'), func_get_args());
  47. }
  48. /**
  49. * translate a string
  50. *
  51. * @access public
  52. * @static
  53. * @param string $messageId
  54. * @param mixed $args one or multiple parameters injected into placeholders
  55. * @return string
  56. */
  57. public static function translate($messageId)
  58. {
  59. if (empty($messageId)) return $messageId;
  60. if (count(self::$_translations) === 0) self::loadTranslations();
  61. $messages = $messageId;
  62. if (is_array($messageId))
  63. {
  64. $messageId = count($messageId) > 1 ? $messageId[1] : $messageId[0];
  65. }
  66. if (!array_key_exists($messageId, self::$_translations))
  67. {
  68. self::$_translations[$messageId] = $messages;
  69. }
  70. $args = func_get_args();
  71. if (is_array(self::$_translations[$messageId]))
  72. {
  73. $number = (int) $args[1];
  74. $key = self::_getPluralForm($number);
  75. $max = count(self::$_translations[$messageId]) - 1;
  76. if ($key > $max) $key = $max;
  77. $args[0] = self::$_translations[$messageId][$key];
  78. $args[1] = $number;
  79. }
  80. else
  81. {
  82. $args[0] = self::$_translations[$messageId];
  83. }
  84. return call_user_func_array('sprintf', $args);
  85. }
  86. /**
  87. * loads translations
  88. *
  89. * From: http://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
  90. *
  91. * @access protected
  92. * @static
  93. * @return void
  94. */
  95. public static function loadTranslations()
  96. {
  97. // find a matching translation file
  98. $availableLanguages = array();
  99. $path = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'i18n';
  100. $i18n = dir($path);
  101. while (false !== ($file = $i18n->read()))
  102. {
  103. if (preg_match('/^([a-z]{2}).json$/', $file, $match) === 1)
  104. {
  105. $availableLanguages[] = $match[1];
  106. }
  107. }
  108. $match = self::_getMatchingLanguage(
  109. self::getBrowserLanguages(), $availableLanguages
  110. );
  111. // load translations
  112. if ($match != 'en')
  113. {
  114. self::$_language = $match;
  115. self::$_translations = json_decode(
  116. file_get_contents($path . DIRECTORY_SEPARATOR . $match . '.json'),
  117. true
  118. );
  119. }
  120. }
  121. /**
  122. * detect the clients supported languages and return them ordered by preference
  123. *
  124. * From: http://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
  125. *
  126. * @return array
  127. */
  128. public static function getBrowserLanguages()
  129. {
  130. $languages = array();
  131. if (array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER))
  132. {
  133. $languageRanges = explode(',', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']));
  134. foreach ($languageRanges as $languageRange) {
  135. if (preg_match(
  136. '/(\*|[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})))?/',
  137. trim($languageRange), $match
  138. ))
  139. {
  140. if (!isset($match[2]))
  141. {
  142. $match[2] = '1.0';
  143. }
  144. else
  145. {
  146. $match[2] = (string) floatval($match[2]);
  147. }
  148. if (!isset($languages[$match[2]]))
  149. {
  150. $languages[$match[2]] = array();
  151. }
  152. $languages[$match[2]][] = strtolower($match[1]);
  153. }
  154. }
  155. krsort($languages);
  156. }
  157. return $languages;
  158. }
  159. /**
  160. * determines the plural form to use based on current language and given number
  161. *
  162. * From: http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html
  163. *
  164. * @param int $n
  165. * @return int
  166. */
  167. protected static function _getPluralForm($n)
  168. {
  169. switch (self::$_language) {
  170. case 'fr':
  171. return ($n > 1 ? 1 : 0);
  172. case 'pl':
  173. return ($n == 1 ? 0 : $n%10 >= 2 && $n %10 <=4 && ($n%100 < 10 || $n%100 >= 20) ? 1 : 2);
  174. // en, de
  175. default:
  176. return ($n != 1 ? 1 : 0);
  177. }
  178. }
  179. /**
  180. * compares two language preference arrays and returns the preferred match
  181. *
  182. * From: http://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
  183. *
  184. * @param array $acceptedLanguages
  185. * @param array $availableLanguages
  186. * @return string
  187. */
  188. protected static function _getMatchingLanguage($acceptedLanguages, $availableLanguages) {
  189. $matches = array();
  190. $any = false;
  191. foreach ($acceptedLanguages as $acceptedQuality => $acceptedValues) {
  192. $acceptedQuality = floatval($acceptedQuality);
  193. if ($acceptedQuality === 0.0) continue;
  194. foreach ($availableLanguages as $availableValue)
  195. {
  196. $availableQuality = 1.0;
  197. foreach ($acceptedValues as $acceptedValue)
  198. {
  199. if ($acceptedValue === '*')
  200. {
  201. $any = true;
  202. }
  203. $matchingGrade = self::_matchLanguage($acceptedValue, $availableValue);
  204. if ($matchingGrade > 0)
  205. {
  206. $q = (string) ($acceptedQuality * $availableQuality * $matchingGrade);
  207. if (!isset($matches[$q]))
  208. {
  209. $matches[$q] = array();
  210. }
  211. if (!in_array($availableValue, $matches[$q]))
  212. {
  213. $matches[$q][] = $availableValue;
  214. }
  215. }
  216. }
  217. }
  218. }
  219. if (count($matches) === 0 && $any)
  220. {
  221. if (count($availableLanguages) > 0)
  222. {
  223. $matches['1.0'] = $availableLanguages;
  224. }
  225. }
  226. if (count($matches) === 0)
  227. {
  228. return 'en';
  229. }
  230. krsort($matches);
  231. $topmatches = current($matches);
  232. return current($topmatches);
  233. }
  234. /**
  235. * compare two language IDs and return the degree they match
  236. *
  237. * From: http://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
  238. *
  239. * @param string $a
  240. * @param string $b
  241. * @return float
  242. */
  243. protected static function _matchLanguage($a, $b) {
  244. $a = explode('-', $a);
  245. $b = explode('-', $b);
  246. for ($i=0, $n=min(count($a), count($b)); $i<$n; $i++)
  247. {
  248. if ($a[$i] !== $b[$i]) break;
  249. }
  250. return $i === 0 ? 0 : (float) $i / count($a);
  251. }
  252. }