You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

359 lines
8.6 KiB

  1. <?php
  2. /**
  3. * Class for a set of entries for translation and their associated headers
  4. *
  5. * @version $Id: translations.php 1157 2015-11-20 04:30:11Z dd32 $
  6. * @package pomo
  7. * @subpackage translations
  8. */
  9. require_once dirname(__FILE__) . '/entry.php';
  10. if ( ! class_exists( 'Translations', false ) ):
  11. class Translations {
  12. var $entries = array();
  13. var $headers = array();
  14. /**
  15. * Add entry to the PO structure
  16. *
  17. * @param array|Translation_Entry &$entry
  18. * @return bool true on success, false if the entry doesn't have a key
  19. */
  20. function add_entry($entry) {
  21. if (is_array($entry)) {
  22. $entry = new Translation_Entry($entry);
  23. }
  24. $key = $entry->key();
  25. if (false === $key) return false;
  26. $this->entries[$key] = &$entry;
  27. return true;
  28. }
  29. /**
  30. * @param array|Translation_Entry $entry
  31. * @return bool
  32. */
  33. function add_entry_or_merge($entry) {
  34. if (is_array($entry)) {
  35. $entry = new Translation_Entry($entry);
  36. }
  37. $key = $entry->key();
  38. if (false === $key) return false;
  39. if (isset($this->entries[$key]))
  40. $this->entries[$key]->merge_with($entry);
  41. else
  42. $this->entries[$key] = &$entry;
  43. return true;
  44. }
  45. /**
  46. * Sets $header PO header to $value
  47. *
  48. * If the header already exists, it will be overwritten
  49. *
  50. * TODO: this should be out of this class, it is gettext specific
  51. *
  52. * @param string $header header name, without trailing :
  53. * @param string $value header value, without trailing \n
  54. */
  55. function set_header($header, $value) {
  56. $this->headers[$header] = $value;
  57. }
  58. /**
  59. * @param array $headers
  60. */
  61. function set_headers($headers) {
  62. foreach($headers as $header => $value) {
  63. $this->set_header($header, $value);
  64. }
  65. }
  66. /**
  67. * @param string $header
  68. */
  69. function get_header($header) {
  70. return isset($this->headers[$header])? $this->headers[$header] : false;
  71. }
  72. /**
  73. * @param Translation_Entry $entry
  74. */
  75. function translate_entry(&$entry) {
  76. $key = $entry->key();
  77. return isset($this->entries[$key])? $this->entries[$key] : false;
  78. }
  79. /**
  80. * @param string $singular
  81. * @param string $context
  82. * @return string
  83. */
  84. function translate($singular, $context=null) {
  85. $entry = new Translation_Entry(array('singular' => $singular, 'context' => $context));
  86. $translated = $this->translate_entry($entry);
  87. return ($translated && !empty($translated->translations))? $translated->translations[0] : $singular;
  88. }
  89. /**
  90. * Given the number of items, returns the 0-based index of the plural form to use
  91. *
  92. * Here, in the base Translations class, the common logic for English is implemented:
  93. * 0 if there is one element, 1 otherwise
  94. *
  95. * This function should be overridden by the sub-classes. For example MO/PO can derive the logic
  96. * from their headers.
  97. *
  98. * @param integer $count number of items
  99. */
  100. function select_plural_form($count) {
  101. return 1 == $count? 0 : 1;
  102. }
  103. /**
  104. * @return int
  105. */
  106. function get_plural_forms_count() {
  107. return 2;
  108. }
  109. /**
  110. * @param string $singular
  111. * @param string $plural
  112. * @param int $count
  113. * @param string $context
  114. */
  115. function translate_plural($singular, $plural, $count, $context = null) {
  116. $entry = new Translation_Entry(array('singular' => $singular, 'plural' => $plural, 'context' => $context));
  117. $translated = $this->translate_entry($entry);
  118. $index = $this->select_plural_form($count);
  119. $total_plural_forms = $this->get_plural_forms_count();
  120. if ($translated && 0 <= $index && $index < $total_plural_forms &&
  121. is_array($translated->translations) &&
  122. isset($translated->translations[$index]))
  123. return $translated->translations[$index];
  124. else
  125. return 1 == $count? $singular : $plural;
  126. }
  127. /**
  128. * Merge $other in the current object.
  129. *
  130. * @param Object &$other Another Translation object, whose translations will be merged in this one
  131. * @return void
  132. **/
  133. function merge_with(&$other) {
  134. foreach( $other->entries as $entry ) {
  135. $this->entries[$entry->key()] = $entry;
  136. }
  137. }
  138. /**
  139. * @param object $other
  140. */
  141. function merge_originals_with(&$other) {
  142. foreach( $other->entries as $entry ) {
  143. if ( !isset( $this->entries[$entry->key()] ) )
  144. $this->entries[$entry->key()] = $entry;
  145. else
  146. $this->entries[$entry->key()]->merge_with($entry);
  147. }
  148. }
  149. }
  150. class Gettext_Translations extends Translations {
  151. /**
  152. * The gettext implementation of select_plural_form.
  153. *
  154. * It lives in this class, because there are more than one descendand, which will use it and
  155. * they can't share it effectively.
  156. *
  157. * @param int $count
  158. */
  159. function gettext_select_plural_form($count) {
  160. if (!isset($this->_gettext_select_plural_form) || is_null($this->_gettext_select_plural_form)) {
  161. list( $nplurals, $expression ) = $this->nplurals_and_expression_from_header($this->get_header('Plural-Forms'));
  162. $this->_nplurals = $nplurals;
  163. $this->_gettext_select_plural_form = $this->make_plural_form_function($nplurals, $expression);
  164. }
  165. return call_user_func($this->_gettext_select_plural_form, $count);
  166. }
  167. /**
  168. * @param string $header
  169. * @return array
  170. */
  171. function nplurals_and_expression_from_header($header) {
  172. if (preg_match('/^\s*nplurals\s*=\s*(\d+)\s*;\s+plural\s*=\s*(.+)$/', $header, $matches)) {
  173. $nplurals = (int)$matches[1];
  174. $expression = trim($this->parenthesize_plural_exression($matches[2]));
  175. return array($nplurals, $expression);
  176. } else {
  177. return array(2, 'n != 1');
  178. }
  179. }
  180. /**
  181. * Makes a function, which will return the right translation index, according to the
  182. * plural forms header
  183. * @param int $nplurals
  184. * @param string $expression
  185. */
  186. function make_plural_form_function($nplurals, $expression) {
  187. $expression = str_replace('n', '$n', $expression);
  188. $func_body = "
  189. \$index = (int)($expression);
  190. return (\$index < $nplurals)? \$index : $nplurals - 1;";
  191. return create_function('$n', $func_body);
  192. }
  193. /**
  194. * Adds parentheses to the inner parts of ternary operators in
  195. * plural expressions, because PHP evaluates ternary oerators from left to right
  196. *
  197. * @param string $expression the expression without parentheses
  198. * @return string the expression with parentheses added
  199. */
  200. function parenthesize_plural_exression($expression) {
  201. $expression .= ';';
  202. $res = '';
  203. $depth = 0;
  204. for ($i = 0; $i < strlen($expression); ++$i) {
  205. $char = $expression[$i];
  206. switch ($char) {
  207. case '?':
  208. $res .= ' ? (';
  209. $depth++;
  210. break;
  211. case ':':
  212. $res .= ') : (';
  213. break;
  214. case ';':
  215. $res .= str_repeat(')', $depth) . ';';
  216. $depth= 0;
  217. break;
  218. default:
  219. $res .= $char;
  220. }
  221. }
  222. return rtrim($res, ';');
  223. }
  224. /**
  225. * @param string $translation
  226. * @return array
  227. */
  228. function make_headers($translation) {
  229. $headers = array();
  230. // sometimes \ns are used instead of real new lines
  231. $translation = str_replace('\n', "\n", $translation);
  232. $lines = explode("\n", $translation);
  233. foreach($lines as $line) {
  234. $parts = explode(':', $line, 2);
  235. if (!isset($parts[1])) continue;
  236. $headers[trim($parts[0])] = trim($parts[1]);
  237. }
  238. return $headers;
  239. }
  240. /**
  241. * @param string $header
  242. * @param string $value
  243. */
  244. function set_header($header, $value) {
  245. parent::set_header($header, $value);
  246. if ('Plural-Forms' == $header) {
  247. list( $nplurals, $expression ) = $this->nplurals_and_expression_from_header($this->get_header('Plural-Forms'));
  248. $this->_nplurals = $nplurals;
  249. $this->_gettext_select_plural_form = $this->make_plural_form_function($nplurals, $expression);
  250. }
  251. }
  252. }
  253. endif;
  254. if ( ! class_exists( 'NOOP_Translations', false ) ):
  255. /**
  256. * Provides the same interface as Translations, but doesn't do anything
  257. */
  258. class NOOP_Translations {
  259. var $entries = array();
  260. var $headers = array();
  261. function add_entry($entry) {
  262. return true;
  263. }
  264. /**
  265. *
  266. * @param string $header
  267. * @param string $value
  268. */
  269. function set_header($header, $value) {
  270. }
  271. /**
  272. *
  273. * @param array $headers
  274. */
  275. function set_headers($headers) {
  276. }
  277. /**
  278. * @param string $header
  279. * @return false
  280. */
  281. function get_header($header) {
  282. return false;
  283. }
  284. /**
  285. * @param Translation_Entry $entry
  286. * @return false
  287. */
  288. function translate_entry(&$entry) {
  289. return false;
  290. }
  291. /**
  292. * @param string $singular
  293. * @param string $context
  294. */
  295. function translate($singular, $context=null) {
  296. return $singular;
  297. }
  298. /**
  299. *
  300. * @param int $count
  301. * @return bool
  302. */
  303. function select_plural_form($count) {
  304. return 1 == $count? 0 : 1;
  305. }
  306. /**
  307. * @return int
  308. */
  309. function get_plural_forms_count() {
  310. return 2;
  311. }
  312. /**
  313. * @param string $singular
  314. * @param string $plural
  315. * @param int $count
  316. * @param string $context
  317. */
  318. function translate_plural($singular, $plural, $count, $context = null) {
  319. return 1 == $count? $singular : $plural;
  320. }
  321. /**
  322. * @param object $other
  323. */
  324. function merge_with(&$other) {
  325. }
  326. }
  327. endif;