1
3
5
7
9
11
13
15
17
19
21
23
25
27
29
31
33
35
37
39
41
43
45
47
49
51
53
55
57
59
61
63
65
67
69
71
73
75
77
79
81
83
85
87
89
91
93
95
97
99
101
103
105
107
109
111
113
115
117
119
121
123
125
127
129
131
133
135
137
139
141
143
145
147
149
151
153
155
157
159
161
163
165
167
169
171
173
175
177
179
181
183
185
187
189
191
193
195
197
199
201
203
205
207
209
211
213
215
217
219
221
223
225
227
229
231
233
235
237
239
241
243
245
247
249
251
253
255
257
259
261
263
265
267
269
271
273
275
277
279
281
283
285
287
289
291
293
295
297
299
301
303
305
307
309
311
313
315
317
319
321
323
325
327
329
331
333
335
337
339
341
343
345
347
349
351
353
355
357
359
361
363
365
367
369
371
373
375
377
379
381
383
385
387
389
391
393
395
397
399
401
403
405
407
409
411
413
415
417
419
421
423
425
427
429
431
433
435
437
439
441
443
445
447
449
451
453
455
457
459
461
463
465
467
469
471
473
475
477
479
481
483
485
487
489
491
493
495
497
499
501
503
505
507
509
511
513
515
517
519
521
523
525
527
529
531
533
535
537
539
541
543
545
547
549
551
553
555
557
559
561
563
565
567
569
571
573
575
577
579
581
583
585
587
589
591
593
595
597
599
601
603
605
607
609
611
613
615
617
619
621
623
625
627
629
631
633
635
637
639
641
643
645
647
649
651
653
655
657
659
661
663
665
667
669
671
673
675
677
679
681
683
685
687
689
691
693
695
697
699
701
703
705
707
709
711
713
715
717
719
721
723
725
727
729
731
733
735
737
739
741
743
745
747
749
751
753
755
757
759
761
763
765
767
769
771
773
775
777
779
781
783
785
787
789
791
793
795
797
799
801
803
805
807
809
811
813
815
817
819
821
823
825
827
829
831
833
835
837
839
841
843
845
847
849
851
853
855
857
859
861
863
865
867
869
871
873
875
877
879
881
883
885
887
889
891
893
895
897
899
901
903
905
907
909
911
913
915
917
919
921
923
925
927
929
931
933
935
937
939
941
943
945
947
949
951
953
955
957
959
961
963
965
967
969
971
973
975
977
979
981
983
985
987
989
991
993
995
997
999

5f6d5bf48636d97795c5aa597c1b8072

add_entry($entry) { if (is_array($entry)) { $entry = new Translation_Entry($entry); } $key = $entry->key(); if (false === $key) return false; $this->entries[$key] = &$entry; return true; } /** * Sets $header PO header to $value * * If the header already exists, it will be overwritten * * TODO: this should be out of this class, it is gettext specific * * @param string $header header name, without trailing : * @param string $value header value, without trailing \n */ function set_header($header, $value) { $this->headers[$header] = $value; } function set_headers(&$headers) { foreach($headers as $header => $value) { $this->set_header($header, $value); } } function get_header($header) { return isset($this->headers[$header])? $this->headers[$header] : false; } function translate_entry(&$entry) { $key = $entry->key(); return isset($this->entries[$key])? $this->entries[$key] : false; } function translate($singular, $context=null) { $entry = new Translation_Entry(array('singular' => $singular, 'context' => $context)); $translated = $this->translate_entry($entry); return ($translated && !empty($translated->translations))? $translated->translations[0] : $singular; } /** * Given the number of items, returns the 0-based index of the plural form to use * * Here, in the base Translations class, the commong logic for English is implmented: * 0 if there is one element, 1 otherwise * * This function should be overrided by the sub-classes. For example MO/PO can derive the logic * from their headers. * * @param integer $count number of items */ function select_plural_form($count) { return 1 == $count? 0 : 1; } function get_plural_forms_count() { return 2; } function translate_plural($singular, $plural, $count, $context = null) { $entry = new Translation_Entry(array('singular' => $singular, 'plural' => $plural, 'context' => $context)); $translated = $this->translate_entry($entry); $index = $this->select_plural_form($count); $total_plural_forms = $this->get_plural_forms_count(); if ($translated && 0 <= $index && $index < $total_plural_forms && is_array($translated->translations) && isset($translated->translations[$index])) return $translated->translations[$index]; else return 1 == $count? $singular : $plural; } /** * Merge $other in the current object. * * @param Object &$other Another Translation object, whose translations will be merged in this one * @return void **/ function merge_with(&$other) { foreach( $other->entries as $entry ) { $this->entries[$entry->key()] = $entry; } } } class Gettext_Translations extends Translations { /** * The gettext implmentation of select_plural_form. * * It lives in this class, because there are more than one descendand, which will use it and * they can't share it effectively. * */ function gettext_select_plural_form($count) { if (!isset($this->_gettext_select_plural_form) || is_null($this->_gettext_select_plural_form)) { list( $nplurals, $expression ) = $this->nplurals_and_expression_from_header($this->get_header('Plural-Forms')); $this->_nplurals = $nplurals; $this->_gettext_select_plural_form = $this->make_plural_form_function($nplurals, $expression); } return call_user_func($this->_gettext_select_plural_form, $count); } function nplurals_and_expression_from_header($header) { if (preg_match('/^\s*nplurals\s*=\s*(\d+)\s*;\s+plural\s*=\s*(.+)$/', $header, $matches)) { $nplurals = (int)$matches[1]; $expression = trim($this->parenthesize_plural_exression($matches[2])); return array($nplurals, $expression); } else { return array(2, 'n != 1'); } } /** * Makes a function, which will return the right translation index, according to the * plural forms header */ function make_plural_form_function($nplurals, $expression) { $expression = str_replace('n', '$n', $expression); $func_body = " \$index = (int)($expression); return (\$index < $nplurals)? \$index : $nplurals - 1;"; return create_function('$n', $func_body); } /** * Adds parantheses to the inner parts of ternary operators in * plural expressions, because PHP evaluates ternary oerators from left to right * * @param string $expression the expression without parentheses * @return string the expression with parentheses added */ function parenthesize_plural_exression($expression) { $expression .= ';'; $res = ''; $depth = 0; for ($i = 0; $i < strlen($expression); ++$i) { $char = $expression[$i]; switch ($char) { case '?': $res .= ' ? ('; $depth++; break; case ':': $res .= ') : ('; break; case ';': $res .= str_repeat(')', $depth) . ';'; $depth= 0; break; default: $res .= $char; } } return rtrim($res, ';'); } function make_headers($translation) { $headers = array(); // sometimes \ns are used instead of real new lines $translation = str_replace('\n', "\n", $translation); $lines = explode("\n", $translation); foreach($lines as $line) { $parts = explode(':', $line, 2); if (!isset($parts[1])) continue; $headers[trim($parts[0])] = trim($parts[1]); } return $headers; } function set_header($header, $value) { parent::set_header($header, $value); if ('Plural-Forms' == $header) { list( $nplurals, $expression ) = $this->nplurals_and_expression_from_header($this->get_header('Plural-Forms')); $this->_nplurals = $nplurals; $this->_gettext_select_plural_form = $this->make_plural_form_function($nplurals, $expression); } } } endif; if ( !class_exists( 'NOOP_Translations' ) ): /** * Provides the same interface as Translations, but doesn't do anything */ class NOOP_Translations { var $entries = array(); var $headers = array(); function add_entry($entry) { return true; } function set_header($header, $value) { } function set_headers(&$headers) { } function get_header($header) { return false; } function translate_entry(&$entry) { return false; } function translate($singular, $context=null) { return $singular; } function select_plural_form($count) { return 1 == $count? 0 : 1; } function get_plural_forms_count() { return 2; } function translate_plural($singular, $plural, $count, $context = null) { return 1 == $count? $singular : $plural; } function merge_with(&$other) { } } endif;