translations.php 8.6 KB

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