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 * @since 2.8.0
9 */
10
11require_once __DIR__ . '/plural-forms.php';
12require_once __DIR__ . '/entry.php';
13
14if ( ! class_exists( 'Translations', false ) ) :
15 /**
16 * Translations class.
17 *
18 * @since 2.8.0
19 */
20 #[AllowDynamicProperties]
21 class Translations {
22 /**
23 * List of translation entries.
24 *
25 * @since 2.8.0
26 *
27 * @var Translation_Entry[]
28 */
29 public $entries = array();
30
31 /**
32 * List of translation headers.
33 *
34 * @since 2.8.0
35 *
36 * @var array<string, string>
37 */
38 public $headers = array();
39
40 /**
41 * Adds an entry to the PO structure.
42 *
43 * @since 2.8.0
44 *
45 * @param array|Translation_Entry $entry
46 * @return bool True on success, false if the entry doesn't have a key.
47 */
48 public function add_entry( $entry ) {
49 if ( is_array( $entry ) ) {
50 $entry = new Translation_Entry( $entry );
51 }
52 $key = $entry->key();
53 if ( false === $key ) {
54 return false;
55 }
56 $this->entries[ $key ] = &$entry;
57 return true;
58 }
59
60 /**
61 * Adds or merges an entry to the PO structure.
62 *
63 * @since 2.8.0
64 *
65 * @param array|Translation_Entry $entry
66 * @return bool True on success, false if the entry doesn't have a key.
67 */
68 public function add_entry_or_merge( $entry ) {
69 if ( is_array( $entry ) ) {
70 $entry = new Translation_Entry( $entry );
71 }
72 $key = $entry->key();
73 if ( false === $key ) {
74 return false;
75 }
76 if ( isset( $this->entries[ $key ] ) ) {
77 $this->entries[ $key ]->merge_with( $entry );
78 } else {
79 $this->entries[ $key ] = &$entry;
80 }
81 return true;
82 }
83
84 /**
85 * Sets $header PO header to $value
86 *
87 * If the header already exists, it will be overwritten
88 *
89 * TODO: this should be out of this class, it is gettext specific
90 *
91 * @since 2.8.0
92 *
93 * @param string $header header name, without trailing :
94 * @param string $value header value, without trailing \n
95 */
96 public function set_header( $header, $value ) {
97 $this->headers[ $header ] = $value;
98 }
99
100 /**
101 * Sets translation headers.
102 *
103 * @since 2.8.0
104 *
105 * @param array $headers Associative array of headers.
106 */
107 public function set_headers( $headers ) {
108 foreach ( $headers as $header => $value ) {
109 $this->set_header( $header, $value );
110 }
111 }
112
113 /**
114 * Returns a given translation header.
115 *
116 * @since 2.8.0
117 *
118 * @param string $header
119 * @return string|false Header if it exists, false otherwise.
120 */
121 public function get_header( $header ) {
122 return isset( $this->headers[ $header ] ) ? $this->headers[ $header ] : false;
123 }
124
125 /**
126 * Returns a given translation entry.
127 *
128 * @since 2.8.0
129 *
130 * @param Translation_Entry $entry Translation entry.
131 * @return Translation_Entry|false Translation entry if it exists, false otherwise.
132 */
133 public function translate_entry( &$entry ) {
134 $key = $entry->key();
135 return isset( $this->entries[ $key ] ) ? $this->entries[ $key ] : false;
136 }
137
138 /**
139 * Translates a singular string.
140 *
141 * @since 2.8.0
142 *
143 * @param string $singular
144 * @param string $context
145 * @return string
146 */
147 public function translate( $singular, $context = null ) {
148 $entry = new Translation_Entry(
149 array(
150 'singular' => $singular,
151 'context' => $context,
152 )
153 );
154 $translated = $this->translate_entry( $entry );
155 return ( $translated && ! empty( $translated->translations ) ) ? $translated->translations[0] : $singular;
156 }
157
158 /**
159 * Given the number of items, returns the 0-based index of the plural form to use
160 *
161 * Here, in the base Translations class, the common logic for English is implemented:
162 * 0 if there is one element, 1 otherwise
163 *
164 * This function should be overridden by the subclasses. For example MO/PO can derive the logic
165 * from their headers.
166 *
167 * @since 2.8.0
168 *
169 * @param int $count Number of items.
170 * @return int Plural form to use.
171 */
172 public function select_plural_form( $count ) {
173 return 1 === (int) $count ? 0 : 1;
174 }
175
176 /**
177 * Returns the plural forms count.
178 *
179 * @since 2.8.0
180 *
181 * @return int Plural forms count.
182 */
183 public function get_plural_forms_count() {
184 return 2;
185 }
186
187 /**
188 * Translates a plural string.
189 *
190 * @since 2.8.0
191 *
192 * @param string $singular
193 * @param string $plural
194 * @param int $count
195 * @param string $context
196 * @return string
197 */
198 public function translate_plural( $singular, $plural, $count, $context = null ) {
199 $entry = new Translation_Entry(
200 array(
201 'singular' => $singular,
202 'plural' => $plural,
203 'context' => $context,
204 )
205 );
206 $translated = $this->translate_entry( $entry );
207 $index = $this->select_plural_form( $count );
208 $total_plural_forms = $this->get_plural_forms_count();
209 if ( $translated && 0 <= $index && $index < $total_plural_forms &&
210 is_array( $translated->translations ) &&
211 isset( $translated->translations[ $index ] ) ) {
212 return $translated->translations[ $index ];
213 } else {
214 return 1 === (int) $count ? $singular : $plural;
215 }
216 }
217
218 /**
219 * Merges other translations into the current one.
220 *
221 * @since 2.8.0
222 *
223 * @param Translations $other Another Translation object, whose translations will be merged in this one (passed by reference).
224 */
225 public function merge_with( &$other ) {
226 foreach ( $other->entries as $entry ) {
227 $this->entries[ $entry->key() ] = $entry;
228 }
229 }
230
231 /**
232 * Merges originals with existing entries.
233 *
234 * @since 2.8.0
235 *
236 * @param Translations $other
237 */
238 public function merge_originals_with( &$other ) {
239 foreach ( $other->entries as $entry ) {
240 if ( ! isset( $this->entries[ $entry->key() ] ) ) {
241 $this->entries[ $entry->key() ] = $entry;
242 } else {
243 $this->entries[ $entry->key() ]->merge_with( $entry );
244 }
245 }
246 }
247 }
248
249 /**
250 * Gettext_Translations class.
251 *
252 * @since 2.8.0
253 */
254 class Gettext_Translations extends Translations {
255
256 /**
257 * Number of plural forms.
258 *
259 * @var int
260 *
261 * @since 2.8.0
262 */
263 public $_nplurals;
264
265 /**
266 * Callback to retrieve the plural form.
267 *
268 * @var callable
269 *
270 * @since 2.8.0
271 */
272 public $_gettext_select_plural_form;
273
274 /**
275 * The gettext implementation of select_plural_form.
276 *
277 * It lives in this class, because there are more than one descendant, which will use it and
278 * they can't share it effectively.
279 *
280 * @since 2.8.0
281 *
282 * @param int $count Plural forms count.
283 * @return int Plural form to use.
284 */
285 public function gettext_select_plural_form( $count ) {
286 if ( ! isset( $this->_gettext_select_plural_form ) || is_null( $this->_gettext_select_plural_form ) ) {
287 list( $nplurals, $expression ) = $this->nplurals_and_expression_from_header( $this->get_header( 'Plural-Forms' ) );
288 $this->_nplurals = $nplurals;
289 $this->_gettext_select_plural_form = $this->make_plural_form_function( $nplurals, $expression );
290 }
291 return call_user_func( $this->_gettext_select_plural_form, $count );
292 }
293
294 /**
295 * Returns the nplurals and plural forms expression from the Plural-Forms header.
296 *
297 * @since 2.8.0
298 *
299 * @param string $header
300 * @return array{0: int, 1: string}
301 */
302 public function nplurals_and_expression_from_header( $header ) {
303 if ( preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s+plural\s*=\s*(.+)$/', $header, $matches ) ) {
304 $nplurals = (int) $matches[1];
305 $expression = trim( $matches[2] );
306 return array( $nplurals, $expression );
307 } else {
308 return array( 2, 'n != 1' );
309 }
310 }
311
312 /**
313 * Makes a function, which will return the right translation index, according to the
314 * plural forms header.
315 *
316 * @since 2.8.0
317 *
318 * @param int $nplurals
319 * @param string $expression
320 * @return callable
321 */
322 public function make_plural_form_function( $nplurals, $expression ) {
323 try {
324 $handler = new Plural_Forms( rtrim( $expression, ';' ) );
325 return array( $handler, 'get' );
326 } catch ( Exception $e ) {
327 // Fall back to default plural-form function.
328 return $this->make_plural_form_function( 2, 'n != 1' );
329 }
330 }
331
332 /**
333 * Adds parentheses to the inner parts of ternary operators in
334 * plural expressions, because PHP evaluates ternary operators from left to right
335 *
336 * @since 2.8.0
337 * @deprecated 6.5.0 Use the Plural_Forms class instead.
338 *
339 * @see Plural_Forms
340 *
341 * @param string $expression the expression without parentheses
342 * @return string the expression with parentheses added
343 */
344 public function parenthesize_plural_exression( $expression ) {
345 $expression .= ';';
346 $res = '';
347 $depth = 0;
348 for ( $i = 0; $i < strlen( $expression ); ++$i ) {
349 $char = $expression[ $i ];
350 switch ( $char ) {
351 case '?':
352 $res .= ' ? (';
353 ++$depth;
354 break;
355 case ':':
356 $res .= ') : (';
357 break;
358 case ';':
359 $res .= str_repeat( ')', $depth ) . ';';
360 $depth = 0;
361 break;
362 default:
363 $res .= $char;
364 }
365 }
366 return rtrim( $res, ';' );
367 }
368
369 /**
370 * Prepare translation headers.
371 *
372 * @since 2.8.0
373 *
374 * @param string $translation
375 * @return array<string, string> Translation headers
376 */
377 public function make_headers( $translation ) {
378 $headers = array();
379 // Sometimes \n's are used instead of real new lines.
380 $translation = str_replace( '\n', "\n", $translation );
381 $lines = explode( "\n", $translation );
382 foreach ( $lines as $line ) {
383 $parts = explode( ':', $line, 2 );
384 if ( ! isset( $parts[1] ) ) {
385 continue;
386 }
387 $headers[ trim( $parts[0] ) ] = trim( $parts[1] );
388 }
389 return $headers;
390 }
391
392 /**
393 * Sets translation headers.
394 *
395 * @since 2.8.0
396 *
397 * @param string $header
398 * @param string $value
399 */
400 public function set_header( $header, $value ) {
401 parent::set_header( $header, $value );
402 if ( 'Plural-Forms' === $header ) {
403 list( $nplurals, $expression ) = $this->nplurals_and_expression_from_header( $this->get_header( 'Plural-Forms' ) );
404 $this->_nplurals = $nplurals;
405 $this->_gettext_select_plural_form = $this->make_plural_form_function( $nplurals, $expression );
406 }
407 }
408 }
409endif;
410
411if ( ! class_exists( 'NOOP_Translations', false ) ) :
412 /**
413 * Provides the same interface as Translations, but doesn't do anything.
414 *
415 * @since 2.8.0
416 */
417 #[AllowDynamicProperties]
418 class NOOP_Translations {
419 /**
420 * List of translation entries.
421 *
422 * @since 2.8.0
423 *
424 * @var Translation_Entry[]
425 */
426 public $entries = array();
427
428 /**
429 * List of translation headers.
430 *
431 * @since 2.8.0
432 *
433 * @var array<string, string>
434 */
435 public $headers = array();
436
437 public function add_entry( $entry ) {
438 return true;
439 }
440
441 /**
442 * Sets a translation header.
443 *
444 * @since 2.8.0
445 *
446 * @param string $header
447 * @param string $value
448 */
449 public function set_header( $header, $value ) {
450 }
451
452 /**
453 * Sets translation headers.
454 *
455 * @since 2.8.0
456 *
457 * @param array $headers
458 */
459 public function set_headers( $headers ) {
460 }
461
462 /**
463 * Returns a translation header.
464 *
465 * @since 2.8.0
466 *
467 * @param string $header
468 * @return false
469 */
470 public function get_header( $header ) {
471 return false;
472 }
473
474 /**
475 * Returns a given translation entry.
476 *
477 * @since 2.8.0
478 *
479 * @param Translation_Entry $entry
480 * @return false
481 */
482 public function translate_entry( &$entry ) {
483 return false;
484 }
485
486 /**
487 * Translates a singular string.
488 *
489 * @since 2.8.0
490 *
491 * @param string $singular
492 * @param string $context
493 */
494 public function translate( $singular, $context = null ) {
495 return $singular;
496 }
497
498 /**
499 * Returns the plural form to use.
500 *
501 * @since 2.8.0
502 *
503 * @param int $count
504 * @return int
505 */
506 public function select_plural_form( $count ) {
507 return 1 === (int) $count ? 0 : 1;
508 }
509
510 /**
511 * Returns the plural forms count.
512 *
513 * @since 2.8.0
514 *
515 * @return int
516 */
517 public function get_plural_forms_count() {
518 return 2;
519 }
520
521 /**
522 * Translates a plural string.
523 *
524 * @since 2.8.0
525 *
526 * @param string $singular
527 * @param string $plural
528 * @param int $count
529 * @param string $context
530 * @return string
531 */
532 public function translate_plural( $singular, $plural, $count, $context = null ) {
533 return 1 === (int) $count ? $singular : $plural;
534 }
535
536 /**
537 * Merges other translations into the current one.
538 *
539 * @since 2.8.0
540 *
541 * @param Translations $other
542 */
543 public function merge_with( &$other ) {
544 }
545 }
546endif;
547