File Coverage

blib/lib/Locale/gettext_pp.pm
Criterion Covered Total %
statement 193 358 53.9
branch 58 200 29.0
condition 37 116 31.9
subroutine 41 43 95.3
pod 24 24 100.0
total 353 741 47.6


line stmt bran cond sub pod time code
1             #! /bin/false
2              
3             # vim: set autoindent shiftwidth=4 tabstop=4:
4              
5             # Pure Perl implementation of Uniforum message translation.
6             # Copyright (C) 2002-2026 Guido Flohr <guido.flohr@cantanea.com>,
7             # all rights reserved.
8              
9             # This program is free software: you can redistribute it and/or modify
10             # it under the terms of the GNU General Public License as published by
11             # the Free Software Foundation; either version 3 of the License, or
12             # (at your option) any later version.
13              
14             # This program is distributed in the hope that it will be useful,
15             # but WITHOUT ANY WARRANTY; without even the implied warranty of
16             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17             # GNU General Public License for more details.
18              
19             # You should have received a copy of the GNU General Public License
20             # along with this program. If not, see <http://www.gnu.org/licenses/>.
21              
22             package Locale::gettext_pp;
23              
24 22     22   11775 use strict;
  22         43  
  22         1562  
25              
26             require 5.004;
27              
28 22         2151 use vars qw ($__gettext_pp_default_dir
29             $__gettext_pp_textdomain
30             $__gettext_pp_domain_bindings
31             $__gettext_pp_domain_codeset_bindings
32             $__gettext_pp_domains
33             $__gettext_pp_recoders
34             $__gettext_pp_unavailable_dirs
35             $__gettext_pp_domain_cache
36             $__gettext_pp_alias_cache
37 22     22   266 $__gettext_pp_context_glue);
  22         43  
38              
39 22     22   10664 use locale;
  22         19669  
  22         128  
40 22     22   1029 use File::Spec;
  22         73  
  22         780  
41 22     22   668 use Locale::Messages;
  22         38  
  22         3483  
42              
43             BEGIN {
44 22     22   81 $__gettext_pp_textdomain = 'messages';
45 22         45 $__gettext_pp_domain_bindings = {};
46 22         77 $__gettext_pp_domain_codeset_bindings = {};
47 22         42 $__gettext_pp_domains = {};
48 22         38 $__gettext_pp_recoders = {};
49 22         47 $__gettext_pp_unavailable_dirs = {};
50 22         51 $__gettext_pp_domain_cache = {};
51 22         45 $__gettext_pp_alias_cache = {};
52             # The separator between msgctxt and msgid in a .mo file. */
53 22         55 $__gettext_pp_context_glue = "\004";
54            
55 22         40 $__gettext_pp_default_dir = '';
56            
57 22         60 for my $dir (qw (/usr/share/locale /usr/local/share/locale)) {
58 22 50       759 if (-d $dir) {
59 22         85 $__gettext_pp_default_dir = $dir;
60 22         1322 last;
61             }
62             }
63             }
64              
65             BEGIN {
66 22     22   12086 require POSIX;
67 22         205525 require Exporter;
68 22     22   14563 use IO::Handle;
  22         173453  
  22         6093  
69 22         11639 require Locale::Recode;
70              
71 22         70 local $@;
72 22         83 my ($has_messages, $five_ok);
73            
74 22         1997 $has_messages = eval '&POSIX::LC_MESSAGES';
75              
76 22 50 33     337 unless (defined $has_messages && length $has_messages) {
77 0   0     0 $five_ok = ! grep {my $x = eval "&POSIX::$_" || 0; $x eq '5';}
  0         0  
  0         0  
78             qw (LC_CTYPE
79             LC_NUMERIC
80             LC_TIME
81             LC_COLLATE
82             LC_MONETARY
83             LC_ALL);
84 0 0       0 if ($five_ok) {
85 0         0 $five_ok = POSIX::setlocale (5, '');
86             }
87             }
88            
89 22 50 33     127 if (defined $has_messages && length $has_messages) {
    0          
90 22     1521 1 2361 eval <<'EOF';
  1521         5104  
  1521         6149  
91             sub LC_MESSAGES()
92             {
93             local $!; # Do not clobber errno!
94            
95             return &POSIX::LC_MESSAGES;
96             }
97             EOF
98             } elsif ($five_ok) {
99 0         0 eval <<'EOF';
100             sub LC_MESSAGES()
101             {
102             local $!; # Do not clobber errno!
103              
104             # Hack: POSIX.pm deems LC_MESSAGES an invalid macro until
105             # Perl 5.8.0. However, on LC_MESSAGES should be 5 ...
106             return 5;
107             }
108             EOF
109             } else {
110 0         0 eval <<'EOF';
111             sub LC_MESSAGES()
112             {
113             local $!; # Do not clobber errno!
114              
115             # This fallback value is widely used,
116             # when LC_MESSAGES is not available.
117             return 1729;
118             }
119             EOF
120             }
121             }
122              
123 22     22   150 use vars qw (%EXPORT_TAGS @EXPORT_OK @ISA $VERSION);
  22         38  
  22         102582  
124              
125             %EXPORT_TAGS = (locale_h => [ qw (
126             gettext
127             dgettext
128             dcgettext
129             ngettext
130             dngettext
131             dcngettext
132             pgettext
133             dpgettext
134             dcpgettext
135             npgettext
136             dnpgettext
137             dcnpgettext
138             textdomain
139             bindtextdomain
140             bind_textdomain_codeset
141             )
142             ],
143             libintl_h => [ qw (LC_CTYPE
144             LC_NUMERIC
145             LC_TIME
146             LC_COLLATE
147             LC_MONETARY
148             LC_MESSAGES
149             LC_ALL)
150             ],
151             );
152              
153             @EXPORT_OK = qw (gettext
154             dgettext
155             dcgettext
156             ngettext
157             dngettext
158             dcngettext
159             pgettext
160             dpgettext
161             dcpgettext
162             npgettext
163             dnpgettext
164             dcnpgettext
165             textdomain
166             bindtextdomain
167             bind_textdomain_codeset
168             nl_putenv
169             setlocale
170             LC_CTYPE
171             LC_NUMERIC
172             LC_TIME
173             LC_COLLATE
174             LC_MONETARY
175             LC_MESSAGES
176             LC_ALL);
177             @ISA = qw (Exporter);
178              
179             my $has_nl_langinfo;
180              
181             sub __load_catalog;
182             sub __load_domain;
183             sub __locale_category;
184             sub __untaint_plural_header;
185             sub __compile_plural_function;
186              
187             sub LC_NUMERIC()
188             {
189 2     2 1 6 &POSIX::LC_NUMERIC;
190             }
191              
192             sub LC_CTYPE()
193             {
194 2     2 1 143803 &POSIX::LC_CTYPE;
195             }
196              
197             sub LC_TIME()
198             {
199 2     2 1 7 &POSIX::LC_TIME;
200             }
201              
202             sub LC_COLLATE()
203             {
204 2     2 1 16 &POSIX::LC_COLLATE;
205             }
206              
207             sub LC_MONETARY()
208             {
209 2     2 1 5 &POSIX::LC_MONETARY;
210             }
211              
212             sub LC_ALL()
213             {
214 2     2 1 8 &POSIX::LC_ALL;
215             }
216              
217             sub textdomain(;$)
218             {
219 208     208 1 434 my $new_domain = shift;
220            
221 208 100 100     747 $__gettext_pp_textdomain = $new_domain if defined $new_domain &&
222             length $new_domain;
223            
224 208         567 return $__gettext_pp_textdomain;
225             }
226              
227             sub bindtextdomain($;$)
228             {
229 665     665 1 1548 my ($domain, $directory) = @_;
230              
231 665         1169 my $retval;
232 665 50 33     2455 if (defined $domain && length $domain) {
233 665 100 100     2848 if (defined $directory && length $directory) {
    100          
234 37         156 $retval = $__gettext_pp_domain_bindings->{$domain}
235             = $directory;
236             } elsif (exists $__gettext_pp_domain_bindings->{$domain}) {
237 626         1241 $retval = $__gettext_pp_domain_bindings->{$domain};
238             } else {
239 2         4 $retval = $__gettext_pp_default_dir;
240             }
241 665 50 33     2477 $retval = '/usr/share/locale' unless defined $retval &&
242             length $retval;
243 665         1794 return $retval;
244             } else {
245 0         0 return;
246             }
247             }
248              
249             sub bind_textdomain_codeset($;$)
250             {
251 2     2 1 6 my ($domain, $codeset) = @_;
252            
253 2 50 33     13 if (defined $domain && length $domain) {
254 2 50 33     10 if (defined $codeset && length $codeset) {
    0          
255 2         28 return $__gettext_pp_domain_codeset_bindings->{$domain} = $codeset;
256             } elsif (exists $__gettext_pp_domain_codeset_bindings->{$domain}) {
257 0         0 return $__gettext_pp_domain_codeset_bindings->{$domain};
258             }
259             }
260            
261 0         0 return;
262             }
263              
264             sub gettext($)
265             {
266 97     97 1 222 my ($msgid) = @_;
267              
268 97         230 return dcnpgettext ('', undef, $msgid, undef, undef, undef);
269             }
270              
271             sub dgettext($$)
272             {
273 12     12 1 33 my ($domainname, $msgid) = @_;
274              
275 12         35 return dcnpgettext ($domainname, undef, $msgid, undef, undef, undef);
276             }
277              
278             sub dcgettext($$$)
279             {
280 12     12 1 34 my ($domainname, $msgid, $category) = @_;
281              
282 12         32 return dcnpgettext ($domainname, undef, $msgid, undef, undef, undef);
283             }
284              
285             sub ngettext($$$)
286             {
287 83     83 1 207 my ($msgid, $msgid_plural, $n) = @_;
288              
289 83         219 return dcnpgettext ('', undef, $msgid, $msgid_plural, $n, undef);
290             }
291              
292             sub dngettext($$$$)
293             {
294 83     83 1 141 my ($domainname, $msgid, $msgid_plural, $n) = @_;
295              
296 83         150 return dcnpgettext ($domainname, undef, $msgid, $msgid_plural, $n, undef);
297             }
298              
299             sub dcngettext($$$$$)
300             {
301 83     83 1 226 my ($domainname, $msgid, $msgid_plural, $n, $category) = @_;
302              
303 83         219 return dcnpgettext ($domainname, undef, $msgid, $msgid_plural, $n, , $category);
304             }
305              
306              
307             sub pgettext($$)
308             {
309 3     3 1 8 my ($msgctxt, $msgid) = @_;
310              
311 3         6 return dcnpgettext ('', $msgctxt, $msgid, undef, undef, undef);
312             }
313              
314             sub dpgettext($$$)
315             {
316 4     4 1 8 my ($domainname, $msgctxt, $msgid) = @_;
317              
318 4         8 return dcnpgettext ($domainname, $msgctxt, $msgid, undef, undef, undef);
319             }
320              
321             sub dcpgettext($$$$)
322             {
323 5     5 1 14 my ($domainname, $msgctxt, $msgid, $category) = @_;
324              
325 5         16 return dcnpgettext ($domainname, $msgctxt, $msgid, undef, undef, undef);
326             }
327              
328             sub npgettext($$$$)
329             {
330 91     91 1 187 my ($msgctxt, $msgid, $msgid_plural, $n) = @_;
331              
332 91         184 return dcnpgettext ('', $msgctxt, $msgid, $msgid_plural, $n, undef);
333             }
334              
335             sub dnpgettext($$$$$)
336             {
337 91     91 1 258 my ($domainname, $msgctxt, $msgid, $msgid_plural, $n) = @_;
338              
339 91         266 return dcnpgettext ($domainname, $msgctxt, $msgid, $msgid_plural, $n, undef);
340             }
341              
342             # This is the actual implementation of dncpgettext. It is also used by the
343             # corresponding function in Locale::gettext_dumb.
344             sub _dcnpgettext_impl {
345 664     664   1892 my ($domainname, $msgctxt, $msgid, $msgid_plural, $n, $category,
346             $locale) = @_;
347              
348 664 50       1590 return unless defined $msgid;
349              
350 664         1261 my $plural = defined $msgid_plural;
351 664         19699 Locale::Messages::turn_utf_8_off($msgid);
352 664 100       7889 Locale::Messages::turn_utf_8_off($msgctxt) if defined $msgctxt;
353 664 100       1870 my $msg_ctxt_id = defined $msgctxt ? join($__gettext_pp_context_glue, ($msgctxt, $msgid)) : $msgid;
354            
355 664         3722 local $!; # Do not clobber errno!
356            
357             # This is also done in __load_domain but we need a proper value.
358 664 100 66     3014 $domainname = $__gettext_pp_textdomain
359             unless defined $domainname && length $domainname;
360            
361             # Category is always LC_MESSAGES (other categories are ignored).
362 664         1132 my $category_name = 'LC_MESSAGES';
363 664         16865 $category = LC_MESSAGES;
364              
365 664         1782 my $domains = __load_domain ($domainname, $category, $category_name,
366             $locale);
367            
368 664         1278 my @trans = ();
369 664         1214 my $domain;
370             my $found;
371 664         1615 foreach my $this_domain (@$domains) {
372 0 0 0     0 if ($this_domain && defined $this_domain->{messages}->{$msg_ctxt_id}) {
373 0         0 @trans = @{$this_domain->{messages}->{$msg_ctxt_id}};
  0         0  
374 0         0 shift @trans;
375 0         0 $domain = $this_domain;
376 0         0 $found = 1;
377 0         0 last;
378             }
379             }
380 664 50       2104 @trans = ($msgid, $msgid_plural) unless @trans;
381            
382 664         1192 my $trans = $trans[0];
383 664 100       1501 if ($plural) {
384 522 50       965 if ($domain) {
385 0         0 my $nplurals = 0;
386 0         0 ($nplurals, $plural) = &{$domain->{plural_func}} ($n);
  0         0  
387 0 0       0 $plural = 0 unless defined $plural;
388 0 0       0 $nplurals = 0 unless defined $nplurals;
389 0 0       0 $plural = 0 if $nplurals <= $plural;
390             } else {
391 522   100     1289 $plural = $n != 1 || 0;
392             }
393            
394 522 50       1326 $trans = $trans[$plural] if defined $trans[$plural];
395             }
396            
397 664 50 33     1595 if ($found && defined $domain->{po_header}->{charset}) {
398 0         0 my $input_codeset = $domain->{po_header}->{charset};
399             # Convert into output charset.
400 0         0 my $output_codeset = $__gettext_pp_domain_codeset_bindings->{$domainname};
401              
402 0 0       0 $output_codeset = $ENV{OUTPUT_CHARSET} unless defined $output_codeset;
403             $output_codeset = __get_codeset ($category, $category_name,
404             $domain->{locale_id})
405 0 0       0 unless defined $output_codeset;
406            
407 0 0       0 unless (defined $output_codeset) {
408             # Still no point.
409 0         0 my $lc_ctype = __locale_category (POSIX::LC_CTYPE(),
410             'LC_CTYPE');
411 0 0       0 $output_codeset = $1
412             if $lc_ctype =~ /^[a-z]{2}(?:_[A-Z]{2})?\.([^@]+)/;
413             }
414              
415             # No point. :-(
416             $output_codeset = $domain->{po_header}->{charset}
417 0 0       0 unless defined $output_codeset;
418            
419 0 0       0 if (exists $__gettext_pp_domain_cache->{$output_codeset}) {
420 0         0 $output_codeset = $__gettext_pp_domain_cache->{$output_codeset};
421             } else {
422 0 0       0 $output_codeset = 'utf-8' if lc $output_codeset eq 'utf8';
423             $output_codeset =
424 0         0 $__gettext_pp_domain_cache->{$output_codeset} =
425             Locale::Recode->resolveAlias ($output_codeset);
426             }
427            
428 0 0 0     0 if (defined $output_codeset &&
429             $output_codeset ne $domain->{po_header}->{charset}) {
430             # We have to convert.
431 0         0 my $recoder;
432            
433 0 0       0 if (exists
434             $__gettext_pp_recoders->{$input_codeset}->{$output_codeset}) {
435 0         0 $recoder = $__gettext_pp_recoders->{$input_codeset}->{$output_codeset};
436             } else {
437             $recoder =
438 0         0 $__gettext_pp_recoders->{$input_codeset}->{$output_codeset} =
439             Locale::Recode->new (from => $input_codeset,
440             to => $output_codeset,
441             );
442             }
443            
444 0         0 $recoder->recode ($trans);
445             }
446             }
447            
448 664         23450 return $trans;
449             }
450              
451             sub dcnpgettext ($$$$$$) {
452 655     655 1 1370 return &_dcnpgettext_impl;
453             }
454              
455             sub nl_putenv ($)
456             {
457 525     525 1 1120 my ($envspec) = @_;
458 525 50       1319 return unless defined $envspec;
459 525 50       1211 return unless length $envspec;
460 525 50       1328 return if substr ($envspec, 0, 1) eq '=';
461            
462 525         1698 my ($var, $value) = split /=/, $envspec, 2;
463              
464             # In Perl we *could* set empty environment variables even under
465             # MS-DOS, but for compatibility reasons, we implement the
466             # brain-damaged behavior of the Microsoft putenv().
467 525 50       1540 if ($^O eq 'MSWin32') {
468 0 0       0 $value = '' unless defined $value;
469 0 0       0 if (length $value) {
470 0         0 $ENV{$var} = $value;
471             } else {
472 0         0 delete $ENV{$var};
473             }
474             } else {
475 525 100       1053 if (defined $value) {
476 407         3216 $ENV{$var} = $value;
477             } else {
478 118         600 delete $ENV{$var};
479             }
480             }
481              
482 525         1214 return 1;
483             }
484              
485             sub setlocale($;$) {
486 126     126 1 775 require POSIX;
487 126         3389 &POSIX::setlocale;
488             }
489              
490             sub __selected_locales {
491 624     624   1371 my ($locale, $category, $category_name) = @_;
492              
493 624 50       1446 if ($ENV{DEBUGME}) {
494 0         0 $DB::single = 1;
495             }
496 624         1002 my @locales;
497             my $cache_key;
498              
499 624         1639 my $locale_category = __locale_category $category, $category_name;
500 624   33     3277 my $language_preference = !defined $locale_category || !length $locale_category
501             || ($locale_category ne 'C' && $locale_category ne 'POSIX');
502 624         1281 my $language = $ENV{LANGUAGE};
503 624 100       1273 $language = '' if !defined $language;
504 624 0 33     1497 if (!$language_preference || $language eq 'C' || $language eq 'POSIX') {
      33        
505 624         2319 return 'C', 'C'; # No translations desired.
506             }
507              
508 0 0 0     0 if (defined $language && length $language) {
    0          
509 0         0 @locales = split /:/, $language;
510 0         0 $cache_key = $language;
511             } elsif (!defined $locale) {
512             # The system does not have LC_MESSAGES. Guess the value.
513 0         0 @locales = $cache_key = $locale_category;
514             } else {
515 0         0 @locales = $cache_key = $locale;
516             }
517              
518 0         0 return $cache_key, @locales;
519             }
520              
521             sub __extend_locales {
522 35     35   144 my (@locales) = @_;
523              
524 35         109 my @tries = @locales;
525 35         137 my %locale_lookup = map { $_ => $_ } @tries;
  35         190  
526              
527 35         135 foreach my $locale (@locales) {
528 35 50       177 if ($locale =~ /^([a-z][a-z])
529             (?:(_[A-Z][A-Z])?
530             (\.[-_A-Za-z0-9]+)?
531             )?
532             (\@[-_A-Za-z0-9]+)?$/x) {
533            
534 0 0       0 if (defined $3) {
535 0 0       0 defined $2 ?
536             push @tries, $1 . $2 . $3 : push @tries, $1 . $3;
537 0         0 $locale_lookup{$tries[-1]} = $locale;
538             }
539 0 0       0 if (defined $2) {
540 0         0 push @tries, $1 . $2;
541 0         0 $locale_lookup{$1 . $2} = $locale;
542             }
543 0 0       0 if (defined $1) {
544 0 0       0 push @tries, $1 if defined $1;
545 0         0 $locale_lookup{$1} = $locale;
546             }
547             }
548             }
549              
550 35         219 return \@tries, \%locale_lookup;
551             }
552              
553             sub __load_domain {
554 664     664   1592 my ($domainname, $category, $category_name, $locale) = @_;
555              
556             # If no locale was selected for the requested locale category,
557             # l10n is disabled completely. This matches the behavior of GNU
558             # gettext.
559 664 50       15285 if ($category != LC_MESSAGES) {
560             # Not supported.
561 0         0 return [];
562             }
563            
564 664 100 66     4822 if (!defined $locale && $category != 1729
      100        
      66        
565             && !defined $ENV{LANGUAGE} && !length $ENV{LANGUAGE}) {
566 40         132 $locale = POSIX::setlocale ($category);
567 40 50 33     183 if (!defined $locale || 'C' eq $locale || 'POSIX' eq $locale) {
      33        
568 40         97 return [];
569             }
570             }
571            
572 624 50 33     2236 $domainname = $__gettext_pp_textdomain
573             unless defined $domainname && length $domainname;
574              
575 624         1362 my $dir = bindtextdomain ($domainname, '');
576 624 50 33     3440 $dir = $__gettext_pp_default_dir unless defined $dir && length $dir;
577              
578 624 50 33     2129 return [] unless defined $dir && length $dir;
579              
580 624         1369 my ($cache_key, @locales) = __selected_locales $locale, $category, $category_name;
581              
582             # Have we looked that one up already?
583 624         2152 my $domains = $__gettext_pp_domain_cache->{$dir}->{$cache_key}->{$category_name}->{$domainname};
584 624 100       1987 return $domains if defined $domains;
585 35 50       98 return [] unless @locales;
586            
587 35         227 my @dirs = ($dir);
588 35         116 my ($tries, $lookup) = __extend_locales @locales;
589              
590 35 50 33     269 push @dirs, $__gettext_pp_default_dir
591             if $__gettext_pp_default_dir && $dir ne $__gettext_pp_default_dir;
592            
593 35         88 my %seen;
594             my %loaded;
595 35         80 foreach my $basedir (@dirs) {
596 70         141 foreach my $try (@$tries) {
597             # If we had already found a catalog for "xy_XY", do not try it
598             # again.
599 70 50       176 next if $loaded{$try};
600              
601 70         926 my $fulldir = File::Spec->catfile($basedir, $try, $category_name);
602 70 50       331 next if $seen{$fulldir}++;
603              
604             # If the cache for unavailable directories is removed,
605             # the three lines below should be replaced by:
606             # 'next unless -d $fulldir;'
607 70 100       243 next if $__gettext_pp_unavailable_dirs->{$fulldir};
608 37 50 50     844 ++$__gettext_pp_unavailable_dirs->{$fulldir} and next
609             unless -d $fulldir;
610 0         0 my $filename = File::Spec->catfile($fulldir, "$domainname.mo");
611 0         0 my $domain = __load_catalog $filename, $try;
612 0 0       0 next unless $domain;
613            
614 0         0 $loaded{$try} = 1;
615              
616 0         0 $domain->{locale_id} = $lookup->{$try};
617 0         0 push @$domains, $domain;
618             }
619             }
620              
621 35 50       169 $domains = [] unless defined $domains;
622            
623             $__gettext_pp_domain_cache->{$dir}
624             ->{$cache_key}
625             ->{$category_name}
626 35         215 ->{$domainname} = $domains;
627              
628 35         276 return $domains;
629             }
630              
631             sub __load_catalog
632             {
633 0     0   0 my ($filename, $locale) = @_;
634            
635             # Alternatively we could check the filename for evil characters ...
636             # (Important for CGIs).
637 0 0 0     0 return unless -f $filename && -r $filename;
638            
639 0         0 local $/;
640 0         0 local *HANDLE;
641            
642 0 0       0 open HANDLE, "<$filename"
643             or return;
644 0         0 binmode HANDLE;
645 0         0 my $raw = <HANDLE>;
646 0         0 close HANDLE;
647            
648             # Corrupted?
649 0 0 0     0 return if ! defined $raw || length $raw < 28;
650            
651 0         0 my $filesize = length $raw;
652            
653             # Read the magic number in order to determine the byte order.
654 0         0 my $domain = {
655             filename => $filename
656             };
657 0         0 my $unpack = 'N';
658 0         0 $domain->{magic} = unpack $unpack, substr $raw, 0, 4;
659            
660 0 0       0 if ($domain->{magic} == 0xde120495) {
    0          
661 0         0 $unpack = 'V';
662             } elsif ($domain->{magic} != 0x950412de) {
663 0         0 return;
664             }
665 0         0 my $domain_unpack = $unpack x 6;
666            
667 0         0 my ($revision, $num_strings, $msgids_off, $msgstrs_off,
668             $hash_size, $hash_off) =
669             unpack (($unpack x 6), substr $raw, 4, 24);
670            
671 0         0 my $major = $revision >> 16;
672 0 0       0 return if $major != 0; # Invalid revision number.
673            
674 0         0 $domain->{revision} = $revision;
675 0         0 $domain->{num_strings} = $num_strings;
676 0         0 $domain->{msgids_off} = $msgids_off;
677 0         0 $domain->{msgstrs_off} = $msgstrs_off;
678 0         0 $domain->{hash_size} = $hash_size;
679 0         0 $domain->{hash_off} = $hash_off;
680            
681 0 0       0 return if $msgids_off + 4 * $num_strings > $filesize;
682 0 0       0 return if $msgstrs_off + 4 * $num_strings > $filesize;
683            
684 0         0 my @orig_tab = unpack (($unpack x (2 * $num_strings)),
685             substr $raw, $msgids_off, 8 * $num_strings);
686 0         0 my @trans_tab = unpack (($unpack x (2 * $num_strings)),
687             substr $raw, $msgstrs_off, 8 * $num_strings);
688            
689 0         0 my $messages = {};
690            
691 0         0 for (my $count = 0; $count < 2 * $num_strings; $count += 2) {
692 0         0 my $orig_length = $orig_tab[$count];
693 0         0 my $orig_offset = $orig_tab[$count + 1];
694 0         0 my $trans_length = $trans_tab[$count];
695 0         0 my $trans_offset = $trans_tab[$count + 1];
696            
697 0 0       0 return if $orig_offset + $orig_length > $filesize;
698 0 0       0 return if $trans_offset + $trans_length > $filesize;
699            
700 0         0 my @origs = split /\000/, substr $raw, $orig_offset, $orig_length;
701 0         0 my @trans = split /\000/, substr $raw, $trans_offset, $trans_length;
702            
703             # The singular is the key, the plural plus all translations is the
704             # value.
705 0         0 my $msgid = $origs[0];
706 0 0 0     0 $msgid = '' unless defined $msgid && length $msgid;
707 0         0 my $msgstr = [ $origs[1], @trans ];
708 0         0 $messages->{$msgid} = $msgstr;
709             }
710            
711 0         0 $domain->{messages} = $messages;
712            
713             # Try to find po header information.
714 0         0 my $po_header = {};
715 0         0 my $null_entry = $messages->{''}->[1];
716 0 0       0 if ($null_entry) {
717 0         0 my @lines = split /\n/, $null_entry;
718 0         0 foreach my $line (@lines) {
719 0         0 my ($key, $value) = split /:/, $line, 2;
720 0         0 $key =~ s/-/_/g;
721 0         0 $po_header->{lc $key} = $value;
722             }
723             }
724 0         0 $domain->{po_header} = $po_header;
725            
726 0 0       0 if (exists $domain->{po_header}->{content_type}) {
727 0         0 my $content_type = $domain->{po_header}->{content_type};
728 0 0       0 if ($content_type =~ s/.*=//) {
729 0         0 $domain->{po_header}->{charset} = $content_type;
730             }
731             }
732            
733 0   0     0 my $code = $domain->{po_header}->{plural_forms} || '';
734            
735             # Whitespace, locale-independent.
736 0         0 my $s = '[ \011-\015]';
737              
738             # Untaint the plural header.
739             # Keep line breaks as is (Perl 5_005 compatibility).
740             $code = $domain->{po_header}->{plural_forms}
741 0         0 = __untaint_plural_header $code;
742              
743 0         0 $domain->{plural_func} = __compile_plural_function $code;
744              
745 0 0 0     0 unless (defined $domain->{po_header}->{charset}
      0        
746             && length $domain->{po_header}->{charset}
747             && $locale =~ /^(?:[a-z][a-z])
748             (?:(?:_[A-Z][A-Z])?
749             (\.[-_A-Za-z0-9]+)?
750             )?
751             (?:\@[-_A-Za-z0-9]+)?$/x) {
752 0         0 $domain->{po_header}->{charset} = $1;
753             }
754            
755 0 0       0 if (defined $domain->{po_header}->{charset}) {
756             $domain->{po_header}->{charset} =
757 0         0 Locale::Recode->resolveAlias ($domain->{po_header}->{charset});
758             }
759            
760 0         0 return $domain;
761             }
762              
763             sub __locale_category
764             {
765 624     624   1153 my ($category, $category_name) = @_;
766            
767 624         1074 local $@;
768            
769             # See https://github.com/gflohr/libintl-perl/issues/14!
770 22     22   229 no if $] >= 5.022, warnings => 'locale';
  22         41  
  22         20699  
771            
772 624         1229 my $value = eval {POSIX::setlocale ($category)};
  624         2402  
773            
774             # We support only XPG syntax, i. e.
775             # language[_territory[.codeset]][@modifier].
776 624 0 33     2662 if (defined $value && $value ne 'C' && $value ne 'POSIX'
      33        
      33        
777             && $value !~ /^[a-z][a-z]
778             (?:_[A-Z][A-Z]
779             (?:\.[-_A-Za-z0-9]+)?
780             )?
781             (?:\@[-_A-Za-z0-9]+)?$/x) {
782 0         0 undef $value;
783             }
784              
785 624 50       1412 unless ($value) {
786 0         0 $value = $ENV{LC_ALL};
787 0 0 0     0 $value = $ENV{$category_name} unless defined $value && length $value;
788 0 0 0     0 $value = $ENV{LANG} unless defined $value && length $value;
789 0 0 0     0 return 'C' unless defined $value && length $value;
790             }
791            
792 624         1676 return $value;
793             }
794              
795             sub __get_codeset
796             {
797 0     0   0 my ($category, $category_name, $locale_id) = @_;
798              
799 0         0 local $@;
800 0 0       0 unless (defined $has_nl_langinfo) {
801 0         0 eval {
802 0         0 require I18N::Langinfo;
803             };
804 0         0 $has_nl_langinfo = !$@;
805             }
806              
807 0 0       0 if ($has_nl_langinfo) {
808             # Try to set the locale via the specified id.
809 0         0 my $saved_locale = eval { POSIX::setlocale (LC_ALL) };
  0         0  
810 0         0 my $had_lc_all = exists $ENV{LC_ALL};
811 0 0       0 my $saved_lc_all = $ENV{LC_ALL} if $had_lc_all;
812              
813             # Now try to set the locale via the environment. There is no
814             # point in calling the langinfo routines if this fails.
815 0         0 $ENV{LC_ALL} = $locale_id;
816 0         0 my $codeset;
817 0         0 my $lc_all = eval { POSIX::setlocale (LC_ALL, $locale_id); };
  0         0  
818 0 0       0 $codeset = I18N::Langinfo::langinfo (I18N::Langinfo::CODESET())
819             if defined $lc_all;
820              
821             # Restore environment.
822 0 0       0 if ($saved_locale) {
823 0         0 eval { POSIX::setlocale (LC_ALL, $saved_locale); }
  0         0  
824             }
825 0 0       0 if ($had_lc_all) {
826 0 0       0 $ENV{LC_ALL} = $saved_lc_all if $had_lc_all;
827             } else {
828 0         0 delete $ENV{LC_ALL};
829             }
830 0         0 return $codeset;
831             }
832              
833 0         0 return;
834             }
835            
836             sub __untaint_plural_header {
837 1     1   144495 my ($code) = @_;
838              
839             # Whitespace, locale-independent.
840 1         3 my $s = '[ \t\r\n\013\014]';
841              
842 1 50       87 if ($code =~ m{^($s*
843             nplurals$s*=$s*[0-9]+
844             $s*;$s*
845             plural$s*=$s*(?:$s|[-\?\|\&=!<>+*/\%:;a-zA-Z0-9_\(\)])+
846             )}xms) {
847 1         6 return $1;
848             }
849              
850 0         0 return '';
851             }
852              
853             sub __compile_plural_function {
854 1     1   80 my ($code) = @_;
855              
856             # The leading and trailing space is necessary to be able to match
857             # against word boundaries.
858 1         2 my $plural_func;
859            
860 1 50       4 if (length $code) {
861 1         2 my $code = ' ' . $code . ' ';
862 1         17 $code =~
863             s/(?<=[^_a-zA-Z0-9])[_a-z][_A-Za-z0-9]*(?=[^_a-zA-Z0-9])/\$$&/gs;
864            
865 1         3 $code = "sub { my \$n = shift || 0;
866             my (\$plural, \$nplurals);
867             $code;
868             return (\$nplurals, \$plural ? \$plural : 0); }";
869            
870             # Now try to evaluate the code. There is no need to run the code in
871             # a Safe compartment. The above substitutions should have destroyed
872             # all evil code. Corrections are welcome!
873             #warn $code;
874 1         143 $plural_func = eval $code;
875             #warn $@ if $@;
876 1 50       5 undef $plural_func if $@;
877             }
878            
879             # Default is Germanic plural (which is incorrect for French).
880 1 50       3 $plural_func = eval "sub { (2, 1 != shift || 0) }" unless $plural_func;
881              
882 1         2 return $plural_func;
883             }
884              
885             1;
886              
887             __END__
888              
889             =head1 NAME
890              
891             Locale::gettext_pp - Pure Perl Implementation of Uniforum Message Translation
892              
893             =head1 SYNOPSIS
894              
895             use Locale::gettext_pp qw(:locale_h :libintl_h);
896              
897             gettext $msgid;
898             dgettext $domainname, $msgid;
899             dcgettext $domainname, $msgid, LC_MESSAGES;
900             ngettext $msgid, $msgid_plural, $count;
901             dngettext $domainname, $msgid, $msgid_plural, $count;
902             dcngettext $domainname, $msgid, $msgid_plural, $count, LC_MESSAGES;
903             pgettext $msgctxt, $msgid;
904             dpgettext $domainname, $msgctxt, $msgid;
905             dcpgettext $domainname, $msgctxt, $msgid, LC_MESSAGES;
906             npgettext $msgctxt, $msgid, $msgid_plural, $count;
907             dnpgettext $domainname, $msgctxt, $msgid, $msgid_plural, $count;
908             dcnpgettext $domainname, $msgctxt, $msgid, $msgid_plural, $count, LC_MESSAGES;
909             textdomain $domainname;
910             bindtextdomain $domainname, $directory;
911             bind_textdomain_codeset $domainname, $encoding;
912             my $category = LC_CTYPE;
913             my $category = LC_NUMERIC;
914             my $category = LC_TIME;
915             my $category = LC_COLLATE;
916             my $category = LC_MONETARY;
917             my $category = LC_MESSAGES;
918             my $category = LC_ALL;
919              
920             =head1 DESCRIPTION
921              
922             The module B<Locale::gettext_pp> is the low-level interface to
923             message translation according to the Uniforum approach that is
924             for example used in GNU gettext and Sun's Solaris.
925              
926             Normally you should not use this module directly, but the high
927             level interface Locale::TextDomain(3) that provides a much simpler
928             interface. This description is therefore deliberately kept
929             brief. Please refer to the GNU gettext documentation available at
930             L<http://www.gnu.org/manual/gettext/> for in-depth and background
931             information on the topic.
932              
933             =head1 FUNCTIONS
934              
935             The module exports by default nothing. Every function has to be
936             imported explicitely or via an export tag (L<"EXPORT TAGS">).
937              
938             =over 4
939              
940             =item B<gettext MSGID>
941              
942             See L<Locale::Messages/FUNCTIONS>.
943              
944             =item B<dgettext TEXTDOMAIN, MSGID>
945              
946             See L<Locale::Messages/FUNCTIONS>.
947              
948             =item B<dcgettext TEXTDOMAIN, MSGID, CATEGORY>
949              
950             See L<Locale::Messages/FUNCTIONS>.
951              
952             =item B<ngettext MSGID, MSGID_PLURAL, COUNT>
953              
954             See L<Locale::Messages/FUNCTIONS>.
955              
956             =item B<dngettext TEXTDOMAIN, MSGID, MSGID_PLURAL, COUNT>
957              
958             See L<Locale::Messages/FUNCTIONS>.
959              
960             =item B<dcngettext TEXTDOMAIN, MSGID, MSGID_PLURAL, COUNT, CATEGORY>
961              
962             See L<Locale::Messages/FUNCTIONS>.
963              
964             =item B<pgettext MSGCTXT, MSGID>
965              
966             See L<Locale::Messages/FUNCTIONS>.
967              
968             =item B<dpgettext TEXTDOMAIN, MSGCTXT, MSGID>
969              
970             See L<Locale::Messages/FUNCTIONS>.
971              
972             =item B<dcpgettext TEXTDOMAIN, MSGCTXT, MSGID, CATEGORY>
973              
974             See L<Locale::Messages/FUNCTIONS>.
975              
976             =item B<npgettext MSGCTXT, MSGID, MSGID_PLURAL, COUNT>
977              
978             See L<Locale::Messages/FUNCTIONS>.
979              
980             =item B<dnpgettext TEXTDOMAIN, MSGCTXT, MSGID, MSGID_PLURAL, COUNT>
981              
982             See L<Locale::Messages/FUNCTIONS>.
983              
984             =item B<dcnpgettext TEXTDOMAIN, MSGCTXT, MSGID, MSGID_PLURAL, COUNT, CATEGORY>
985              
986             See L<Locale::Messages/FUNCTIONS>.
987              
988             =item B<textdomain TEXTDOMAIN>
989              
990             See L<Locale::Messages/FUNCTIONS>.
991              
992             =item B<bindtextdomain TEXTDOMAIN, DIRECTORY>
993              
994             See L<Locale::Messages/FUNCTIONS>.
995              
996             =item B<bind_textdomain_codeset TEXTDOMAIN, ENCODING>
997              
998             =item B<nl_putenv ENVSPEC>
999              
1000             See L<Locale::Messages/FUNCTIONS>.
1001              
1002             =item B<setlocale>
1003              
1004             See L<Locale::Messages/FUNCTIONS>.
1005              
1006             =back
1007              
1008             =head1 CONSTANTS
1009              
1010             You can (maybe) get the same constants from POSIX(3); see there for
1011             a detailed description
1012              
1013             =over 4
1014              
1015             =item B<LC_CTYPE>
1016              
1017             =item B<LC_NUMERIC>
1018              
1019             =item B<LC_TIME>
1020              
1021             =item B<LC_COLLATE>
1022              
1023             =item B<LC_MONETARY>
1024              
1025             =item B<LC_MESSAGES>
1026              
1027             =item B<LC_ALL>
1028              
1029             See L<Locale::Messages/CONSTANTS> for more information.
1030              
1031             =back
1032              
1033             =head1 EXPORT TAGS
1034              
1035             This module does not export anything unless explicitely requested.
1036             You can import groups of functions via two tags:
1037              
1038             =over 4
1039              
1040             =item B<use Locale::gettext_pp qw(':locale_h')>
1041              
1042             Imports the functions that are normally defined in the C include
1043             file F<locale.h>:
1044              
1045             =over 8
1046              
1047             =item B<gettext()>
1048              
1049             =item B<dgettext()>
1050              
1051             =item B<dcgettext()>
1052              
1053             =item B<ngettext()>
1054              
1055             =item B<dngettext()>
1056              
1057             =item B<dcngettext()>
1058              
1059             =item B<pgettext()>
1060              
1061             Introduced with libintl-perl 1.17.
1062              
1063             =item B<dpgettext()>
1064              
1065             Introduced with libintl-perl 1.17.
1066              
1067             =item B<dcpgettext()>
1068              
1069             Introduced with libintl-perl 1.17.
1070              
1071             =item B<npgettext()>
1072              
1073             Introduced with libintl-perl 1.17.
1074              
1075             =item B<dnpgettext()>
1076              
1077             Introduced with libintl-perl 1.17.
1078              
1079             =item B<dcnpgettext()>
1080              
1081             Introduced with libintl-perl 1.17.
1082              
1083             =item B<textdomain()>
1084              
1085             =item B<bindtextdomain()>
1086              
1087             =item B<bind_textdomain_codeset()>
1088              
1089             =back
1090              
1091             =item B<use Locale::gettext_pp (':libintl_h')>
1092              
1093             Imports the locale category constants:
1094              
1095             =over 8
1096              
1097             =item B<LC_CTYPE>
1098              
1099             =item B<LC_NUMERIC>
1100              
1101             =item B<LC_TIME>
1102              
1103             =item B<LC_COLLATE>
1104              
1105             =item B<LC_MONETARY>
1106              
1107             =item B<LC_MESSAGES>
1108              
1109             =item B<LC_ALL>
1110              
1111             =back
1112              
1113             =back
1114              
1115             =head1 AUTHOR
1116              
1117             Copyright (C) 2002-2026 L<Guido Flohr|http://www.guido-flohr.net/>
1118             (L<mailto:guido.flohr@cantanea.com>), all rights reserved. See the source
1119             code for details!code for details!
1120              
1121             =head1 SEE ALSO
1122              
1123             Locale::TextDomain(3pm), Locale::Messages(3pm), Encode(3pm),
1124             perllocale(3pm), POSIX(3pm), perl(1), gettext(1), gettext(3)
1125              
1126             =cut
1127              
1128             Local Variables:
1129             mode: perl
1130             perl-indent-level: 4
1131             perl-continued-statement-offset: 4
1132             perl-continued-brace-offset: 0
1133             perl-brace-offset: -4
1134             perl-brace-imaginary-offset: 0
1135             perl-label-offset: -4
1136             tab-width: 4
1137             End: