File Coverage

lib/Text/PO.pm
Criterion Covered Total %
statement 562 856 65.6
branch 197 460 42.8
condition 88 275 32.0
subroutine 69 87 79.3
pod 51 51 100.0
total 967 1729 55.9


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## PO Files Manipulation - ~/lib/Text/PO.pm
3             ## Version v0.9.1
4             ## Copyright(c) 2025 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2018/06/21
7             ## Modified 2025/12/05
8             ## All rights reserved
9             ##
10             ## This program is free software; you can redistribute it and/or modify it
11             ## under the same terms as Perl itself.
12             ##----------------------------------------------------------------------------
13             package Text::PO;
14             BEGIN
15             {
16 5     5   1252279 use strict;
  5         11  
  5         210  
17 5     5   28 use warnings;
  5         19  
  5         276  
18 5     5   29 use warnings::register;
  5         9  
  5         397  
19 5     5   668 use parent qw( Module::Generic );
  5         391  
  5         48  
20 5     5   292960 use vars qw( $VERSION @META $DEF_META );
  5         20  
  5         357  
21 5     5   1226 use open ':std' => ':utf8';
  5         3203  
  5         40  
22 5     5   1697 use Class::Struct;
  5         2681  
  5         63  
23 5     5   8822 use DateTime;
  5         3471112  
  5         308  
24 5     5   54 use DateTime::TimeZone;
  5         11  
  5         132  
25 5     5   34 use Encode ();
  5         9  
  5         129  
26 5     5   26 use Fcntl qw( :DEFAULT );
  5         9  
  5         2136  
27 5     5   5846 use JSON ();
  5         52796  
  5         267  
28 5     5   50 use Scalar::Util;
  5         19  
  5         273  
29 5     5   9296 use Text::PO::Element;
  5         61  
  5         102  
30 5 50   5   2961 use constant HAS_LOCAL_TZ => ( eval( qq{DateTime::TimeZone->new( name => 'local' );} ) ? 1 : 0 );
  5         9  
  5         399  
31 5     5   30813 our $VERSION = 'v0.9.1';
32             };
33              
34 5     5   82 use strict;
  5         24  
  5         159  
35 5     5   28 use warnings;
  5         9  
  5         74059  
36              
37             struct 'Text::PO::Comment' =>
38             {
39             'text' => '@',
40             };
41             our @META = qw(
42             Project-Id-Version
43             Report-Msgid-Bugs-To
44             POT-Creation-Date
45             PO-Revision-Date
46             Last-Translator
47             Language-Team
48             Language
49             Plural-Forms
50             MIME-Version
51             Content-Type
52             Content-Transfer-Encoding
53             );
54             our $DEF_META =
55             {
56             'Project-Id-Version' => 'Project 0.1',
57             'Report-Msgid-Bugs-To' => 'bugs@example.com',
58             # 2011-07-02 20:53+0900
59             'POT-Creation-Date' => DateTime->from_epoch( 'epoch' => time(), 'time_zone' => ( HAS_LOCAL_TZ ? 'local' : 'UTC' ) )->strftime( '%Y-%m-%d %H:%M%z' ),
60             'PO-Revision-Date' => DateTime->from_epoch( 'epoch' => time(), 'time_zone' => ( HAS_LOCAL_TZ ? 'local' : 'UTC' ) )->strftime( '%Y-%m-%d %H:%M%z' ),
61             'Last-Translator' => 'Unknown <hello@example.com>',
62             'Language-Team' => 'Unknown <hello@example.com>',
63             'Language' => '',
64             'Plural-Forms' => 'nplurals=1; plural=0;',
65             'MIME-Version' => '1.0',
66             'Content-Type' => 'text/plain; charset=utf-8',
67             'Content-Transfer-Encoding' => '8bit',
68             };
69              
70             sub init
71             {
72 19     19 1 2123512 my $self = shift( @_ );
73 19         567 $self->{domain} = undef;
74 19         157 $self->{header} = [];
75             # utf8
76 19         152 $self->{encoding} = undef;
77             # Should we allow inclusion ?
78 19         122 $self->{include} = 1;
79             # Maximum recursion allowed for the include option
80 19         120 $self->{max_recurse} = 32;
81 19         161 $self->{meta} = {};
82 19         130 $self->{meta_keys} = [];
83             # Default to using po json file if it exists
84 19         100 $self->{use_json} = 1;
85 19         205 $self->{remove_duplicates} = 1;
86 19         106 $self->{_init_strict_use_sub} = 1;
87 19         203 $self->SUPER::init( @_ );
88 19         47873 $self->{elements} = [];
89 19         140 $self->{added} = [];
90 19         101 $self->{removed} = [];
91 19         147 $self->{source} = {};
92 19         70 $self->{_parsed} = 0;
93 19         64 return( $self );
94             }
95              
96             sub add_element
97             {
98 0     0 1 0 my $self = shift( @_ );
99 0         0 my( $msgid, $e, $opts );
100 0 0       0 if( $self->_is_a( $_[0] => 'Text::PO::Element' ) )
101             {
102 0         0 $e = shift( @_ );
103 0   0     0 $msgid = $e->msgid_as_text || return( $self->error( "No msgid was provided" ) );
104 0         0 $opts = $self->_get_args_as_hash( @_ );
105             }
106             else
107             {
108 0         0 $opts = $self->_get_args_as_hash( @_ );
109 0   0     0 $msgid = $opts->{msgid} || return( $self->error( "No msgid was provided" ) );
110 0   0     0 $e = $self->new_element( %$opts ) || return( $self->pass_error );
111             }
112 0 0       0 return( $self->error( "No msgid was provided." ) ) if( !length( $msgid ) );
113 0         0 my $elems = $self->elements;
114 0         0 foreach my $e2 ( @$elems )
115             {
116 0 0 0     0 next if( $e2->is_meta || $e2->is_include );
117 0 0       0 if( $e2->msgid_as_text eq $msgid )
118             {
119             # return( $self->error( "There already is an id '$msgid' in the po file" ) );
120 0         0 return( $e2 );
121             }
122             }
123 0         0 $e->po( $self );
124 0   0     0 my $id = ( $opts->{before} || $opts->{after} );
125 0 0       0 if( $id )
126             {
127 0         0 my $found = 0;
128 0         0 for( my $i = 0; $i < scalar( @$elems ); $i++ )
129             {
130 0         0 my $elem = $elems->[$i];
131 0 0       0 next if( $elem->is_meta );
132 0 0 0     0 if( ( $elem->is_include && ( $elem->file // '' ) eq $id ) ||
      0        
      0        
      0        
      0        
133             ( !$elem->is_include && ( $elem->id // '' ) eq $id ) )
134             {
135 0 0       0 if( $opts->{after} )
    0          
136             {
137 0         0 splice( @$elems, $i + 1, 0, $e );
138             }
139             elsif( $opts->{before} )
140             {
141 0         0 splice( @$elems, $i, 0, $e );
142             }
143 0         0 $found++;
144 0         0 last;
145             }
146             }
147 0 0       0 if( !$found )
148             {
149 0 0       0 return( $self->error( "No msgid/include '$id', to add ", ( $opts->{before} ? 'before' : 'after' ), ", was found, and thus the msgid '${msgid}' could not be added." ) );
150             }
151             }
152             else
153             {
154 0         0 push( @{$self->{elements}}, $e );
  0         0  
155             }
156 0         0 return( $e );
157             }
158              
159             sub add_include
160             {
161 0     0 1 0 my $self = shift( @_ );
162 0         0 my $e;
163             my $opts;
164 0 0       0 if( $self->_is_a( $_[0] => 'Text::PO::Element' ) )
165             {
166 0         0 $e = shift( @_ );
167 0 0       0 if( !$e->file )
168             {
169 0         0 return( $self->error( "The Text::PO::Element object provided does not have any 'file' value set." ) );
170             }
171 0         0 $opts = $self->_get_args_as_hash( @_ );
172             }
173             else
174             {
175 0         0 $opts = $self->_get_args_as_hash( @_ );
176 0 0       0 if( !$opts->{file} )
177             {
178 0         0 return( $self->error( "No 'file' property found in the hash of options provided." ) );
179             }
180 0   0     0 $e = $self->new_element( %$opts ) || return( $self->pass_error );
181             }
182 0         0 $e->is_include(1);
183 0         0 my $file = $e->file;
184 0         0 my $elems = $self->elements;
185 0         0 foreach my $elem ( @$elems )
186             {
187 0 0       0 next unless( $elem->is_include );
188 0 0 0     0 if( ( $elem->file // '' ) eq $file )
189             {
190 0         0 return( $elem );
191             }
192             }
193              
194 0         0 $e->po( $self );
195 0   0     0 my $id = ( $opts->{before} || $opts->{after} );
196 0 0       0 if( $id )
197             {
198 0         0 my $found = 0;
199 0         0 for( my $i = 0; $i < scalar( @$elems ); $i++ )
200             {
201 0         0 my $elem = $elems->[$i];
202 0 0       0 next if( $elem->is_meta );
203 0 0 0     0 if( ( $elem->is_include && ( $elem->file // '' ) eq $id ) ||
      0        
      0        
      0        
      0        
204             ( !$elem->is_include && ( $elem->id // '' ) eq $id ) )
205             {
206 0 0       0 if( $opts->{after} )
    0          
207             {
208 0         0 splice( @$elems, $i + 1, 0, $e );
209             }
210             elsif( $opts->{before} )
211             {
212 0         0 splice( @$elems, $i, 0, $e );
213             }
214 0         0 $found++;
215 0         0 last;
216             }
217             }
218 0 0       0 if( !$found )
219             {
220 0 0       0 return( $self->error( "No msgid/include '$id', to add ", ( $opts->{before} ? 'before' : 'after' ), ", was found, and thus the include '${file}' could not be added." ) );
221             }
222             }
223             else
224             {
225 0         0 push( @{$self->{elements}}, $e );
  0         0  
226             }
227 0         0 return( $e );
228             }
229              
230 0     0 1 0 sub added { return( shift->_set_get_array_as_object( 'added', @_ ) ); }
231              
232 1     1 1 1941 sub as_hash { return( shift->hash( @_ ) ); }
233              
234             sub as_json
235             {
236 1     1 1 4059 my $self = shift( @_ );
237 1         10 my $opts = $self->_get_args_as_hash( @_ );
238 1         9 my $metaKeys = $self->{meta_keys};
239 1         3 my $hash = {};
240 1         4 $hash->{domain} = $self->domain;
241 1         966 $hash->{meta} = {};
242 1         4 $hash->{meta_keys} = [];
243 1         5 $hash->{elements} = [];
244 1         10 foreach my $k ( @$metaKeys )
245             {
246 11         35 my $key = $self->normalise_meta( $k );
247 11         28 my $val = $self->meta( $k );
248 11         368 $hash->{meta}->{ $key } = $val;
249 11         19 push( @{$hash->{meta_keys}}, $key );
  11         37  
250             }
251 1         5 my $elem = $self->elements;
252 1         931 foreach my $e ( @$elem )
253             {
254 8         31 my $msgid = $e->msgid_as_text;
255 8         25 my $msgstr = $e->msgstr;
256 8 50 50     21 next if( $e->is_meta || !CORE::length( $msgid // '' ) );
      33        
257 8         7578 my $k = $msgid;
258             # my $v = ref( $msgstr ) ? join( '', @$msgstr ) : $msgstr;
259 8         15 my $v;
260 8 100       35 if( $e->plural )
261             {
262 1         974 my $res = [];
263 1         6 for( my $i = 0; $i < scalar( @$msgstr ); $i++ )
264             {
265 2 50       15 push( @$res, ref( $msgstr->[$i] ) ? join( '', @{$msgstr->[$i]} ) : $msgstr->[$i] );
  2         12  
266             }
267 1         3 $v = $res;
268             }
269             else
270             {
271 7 100       6463 $v = ref( $msgstr ) ? join( '', @$msgstr ) : $msgstr;
272             }
273              
274 8         42 my $ref =
275             {
276             msgid => $k,
277             msgstr => $v,
278             };
279 8 100 66     30 $ref->{msgid_plural} = $e->msgid_plural if( $e->plural && $e->msgid_plural );
280 8 50       6929 if( !scalar( @{$ref->{comment} = $e->comment} ) )
  8         42  
281             {
282 8         319 delete( $ref->{comment} );
283             }
284 8 50       29 if( !length( $ref->{context} = $e->context ) )
285             {
286 8         7650 delete( $ref->{context} );
287             }
288 8 50       15 if( !scalar( @{$ref->{flags} = $e->flags} ) )
  8         31  
289             {
290 8         305 delete( $ref->{flags} );
291             }
292 8 100       34 if( !length( $ref->{reference} = $e->reference ) )
293             {
294 5         13 delete( $ref->{reference} );
295             }
296 8         15 push( @{$hash->{elements}}, $ref );
  8         39  
297             }
298 1         75 my $j = JSON->new->relaxed->allow_blessed->convert_blessed;
299             # canonical = sorting hash keys
300 1         5 foreach my $t ( qw( pretty utf8 indent canonical ) )
301             {
302 4 50       11 $j->$t( $opts->{ $t } ) if( exists( $opts->{ $t } ) );
303             }
304 1 50       5 $j->canonical( $opts->{sort} ) if( exists( $opts->{sort} ) );
305             # try-catch
306 1         2 local $@;
307             my $json = eval
308 1         3 {
309 1         43 $j->encode( $hash );
310             };
311 1 50       4 if( $@ )
312             {
313 0         0 return( $self->error( "Unable to json encode the hash data created: $@" ) );
314             }
315 1         31 return( $json );
316             }
317              
318             sub as_string
319             {
320 0     0 1 0 my $self = shift( @_ );
321 0         0 my $s = $self->new_scalar( '' );
322 0   0     0 my $io = $s->open || return( $self->pass_error( $s->error ) );
323 0         0 $self->dump( $io );
324 0         0 return( "$s" );
325             }
326              
327             sub charset
328             {
329 2     2 1 1364 my $self = shift( @_ );
330 2         11 my $type = $self->content_type();
331 2         53 my $def = $self->parse_header_value( $type );
332 2 50       19 if( @_ )
333             {
334 0         0 my $v = shift( @_ );
335 0         0 $def->params->{charset} = $v;
336 0         0 $self->meta( content_type => $def->as_string );
337             }
338 2         34 return( $def->params->{charset} );
339             }
340              
341 2     2 1 2235 sub content_encoding { return( shift->_set_get_meta_value( 'Content-Transfer-Encoding', @_ ) ); }
342              
343 4     4 1 1025 sub content_type { return( shift->_set_get_meta_value( 'Content-Type', @_ ) ); }
344              
345             # <https://superuser.com/questions/392439/lang-and-language-environment-variable-in-debian-based-systems>
346             sub current_lang
347             {
348 2     2 1 993 my $self = shift( @_ );
349 2 50 33     27 return( '' ) if( !CORE::exists( $ENV{LANGUAGE} ) && !CORE::exists( $ENV{LANG} ) );
350 0 0 0     0 return( ( $ENV{LANGUAGE} || $ENV{LANG} ) ? [split( /:/, ( $ENV{LANGUAGE} || $ENV{LANG} ) )]->[0] : '' );
      0        
351             }
352              
353             sub decode
354             {
355 0     0 1 0 my $self = shift( @_ );
356 0         0 my $str = shift( @_ );
357 0 0       0 return( '' ) if( !length( $str ) );
358 0         0 my $enc = $self->encoding;
359 0 0       0 return( $str ) if( !$enc );
360             # try-catch
361 0         0 local $@;
362             my $rv = eval
363 0         0 {
364 0 0       0 return( Encode::decode_utf8( $str, Encode::FB_CROAK ) ) if( $enc eq 'utf8' );
365 0         0 return( Encode::decode( $enc, $str, Encode::FB_CROAK ) );
366             };
367 0 0       0 if( $@ )
368             {
369 0         0 return( $self->error( "An error occurred while trying to decode a string using encoding '$enc': $@" ) );
370             }
371 0         0 return( $rv );
372             }
373              
374 12     12 1 51568 sub domain { return( shift->_set_get_scalar( 'domain', @_ ) ); }
375              
376             sub dump
377             {
378 1     1 1 134739 my $self = shift( @_ );
379 1         19 require IO::File;
380 1         12 my $fh = IO::File->new;
381 1 50       69 if( @_ )
382             {
383 1         5 $fh = shift( @_ );
384 1 50       11 return( $self->error( "Filehandle provided '$fh' (", ref( $fh ), ") does not look like a filehandle" ) ) if( !Scalar::Util::openhandle( $fh ) );
385             # $fh->fdopen( fileno( $fh ), 'w' );
386             }
387             else
388             {
389 0         0 $fh->fdopen( fileno( STDOUT ), 'w' );
390             }
391 1   50     14 my $enc = $self->encoding || 'utf8';
392 1 50       959 $enc = 'utf8' if( lc( $enc ) eq 'utf-8' );
393 1 50       18 $fh->binmode( ":${enc}" ) || return( $self->error( "Unable to set binmode on character encoding '$enc': $!" ) );
394 1         385 $fh->autoflush(1);
395              
396             # If this is a brain new instance whose data do not originate from parsing a file, and we do not have yet meta data, we set some default now
397 1 0 33     370 if( !$self->{_parsed} && !scalar( keys( %{$self->{meta}} ) ) )
  0         0  
398             {
399 0         0 $self->set_default_meta;
400             }
401              
402 1         7 my $elem = $self->{elements};
403 1 50       16 if( my $header = $self->header )
404             {
405 1 50       1481 $fh->print( join( "\n", @$header ) ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
406             }
407 1         470 my $domain = '';
408 1 50       12 $domain = $self->domain if( $self->domain );
409 1 50       931 if( length( $domain ) )
410             {
411 1 50       19 $fh->print( "\n#\n# domain \"${domain}\"" ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
412             }
413 1 50       422 $fh->print( "\n\n" ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
414             # my $metaKeys = $self->meta_keys;
415 1         417 my $metaKeys = [@META];
416 1 50       7 if( scalar( @$metaKeys ) )
417             {
418 1 50       10 $fh->printf( "msgid \"\"\n" ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
419 1 50       390 $fh->printf( "msgstr \"\"\n" ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
420 1         401 foreach my $k ( @$metaKeys )
421             {
422 11         4090 my $k2 = lc( $k );
423 11         28 $k2 =~ tr/-/_/;
424             # No, we do not do this anymore. See set_default_meta()
425             # if( !exists( $self->{meta}->{ $k2 } ) &&
426             # length( $DEF_META->{ $k } ) )
427             # {
428             # $self->{meta}->{ $k2 } = $DEF_META->{ $k };
429             # }
430 11 50       107 next if( !exists( $self->{meta}->{ $k2 } ) );
431 11 50       399 $fh->printf( "\"%s: %s\\n\"\n", $self->normalise_meta( $k ), $self->meta( $k ) ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
432             }
433 1 50       427 $fh->print( "\n" ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
434             }
435 1         366 foreach my $e ( @$elem )
436             {
437 8         2417 my $msgid = $e->msgid;
438 8 50       319 if( $e->is_include )
439             {
440 0 0       0 $fh->print( $e->dump, "\n" ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
441             }
442             else
443             {
444 8 50 33     7482 next if( $e->is_meta || !CORE::length( $msgid ) || ( ref( $msgid // '' ) eq 'ARRAY' && !scalar( @$msgid ) ) );
      50        
      66        
      33        
445 8 50       7529 if( $e->po ne $self )
446             {
447 0 0       0 warn( "This element '", $e->msgid_as_text, "' does not belong to us. Its po object is different than our current object.\n" ) if( $self->_is_warnings_enabled );
448             }
449 8 50       278 $fh->print( $e->dump, "\n" ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
450             }
451 8 50       2863 $fh->print( "\n" ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
452             }
453 1         214 return( $self );
454             }
455              
456 22     22 1 126532 sub elements { return( shift->_set_get_array_as_object( 'elements', @_ ) ); }
457              
458 99     99 1 22476 sub encoding { return( shift->_set_get_scalar( 'encoding', @_ ) ); }
459              
460             sub exists
461             {
462 1     1 1 1054 my $self = shift( @_ );
463 1   50     16 my $elem = shift( @_ ) || return( $self->error( "No element to check existence was provided." ) );
464 1 50       43 return( $self->error( "The element provided is not an Text::PO::Element object" ) ) if( !$self->_is_a( $elem => 'Text::PO::Element' ) );
465 1         47 my $opts = $self->_get_args_as_hash( @_ );
466 1   50     33 $opts->{msgid_only} //= 0;
467 1         8 my $elems = $self->{elements};
468             # No need to go further if the object provided does not even have a msgid
469 1 50 33     8 return(0) if( !$elem->is_include && !length( $elem->msgid_as_text ) );
470 1         17 foreach my $e ( @$elems )
471             {
472 1 50 0     8 if( $e->is_include )
    50 0        
      33        
      50        
      50        
      50        
      50        
      33        
      33        
473             {
474 0 0 0     0 if( ( $e->file // '' ) eq ( $elem->file // '' ) )
      0        
475             {
476 0         0 return(1);
477             }
478             }
479             elsif( ( $opts->{msgid_only} && ( $e->msgid_as_text // '' ) eq ( $elem->msgid_as_text // '' ) ) ||
480             ( ( $e->msgid_as_text // '' ) eq ( $elem->msgid_as_text // '' ) && ( $e->msgstr_as_text // '' ) eq ( $elem->msgstr_as_text // '' ) ) )
481             {
482 1 50       16 if( length( $elem->context ) )
483             {
484 0 0       0 if( $elem->context eq $e->context )
485             {
486 0         0 return(1);
487             }
488             }
489             else
490             {
491 1         988 return(1);
492             }
493             }
494             }
495 0         0 return(0);
496             }
497              
498             sub hash
499             {
500 1     1 1 4 my $self = shift( @_ );
501 1         5 my $elem = $self->elements;
502 1         963 my $hash = {};
503 1         12 foreach my $e ( @$elem )
504             {
505 8         25 my $msgid = $e->msgid_as_text;
506 8         21 my $msgstr = $e->msgstr_as_text;
507 8         31 $hash->{ $msgid } = $msgstr;
508             }
509 1         14 return( $self->new_hash( $hash ) );
510             }
511              
512 12     12 1 144 sub header { return( shift->_set_get_array_as_object( 'header', @_ ) ); }
513              
514 10     10 1 425 sub include { return( shift->_set_get_boolean( 'include', @_ ) ); }
515              
516 8     8 1 96 sub language { return( shift->_set_get_meta_value( 'Language', @_ ) ); }
517              
518 2     2 1 1061 sub language_team { return( shift->_set_get_meta_value( 'Language-Team', @_ ) ); }
519              
520 2     2 1 952 sub last_translator { return( shift->_set_get_meta_value( 'Last-Translator', @_ ) ); }
521              
522             sub max_recurse { return( shift->_set_get_number({
523 10     10 1 413 field => 'max_recurse',
524             constraint => 'unsigned_int',
525             }, @_ ) ); }
526              
527             sub merge
528             {
529 0     0 1 0 my $self = shift( @_ );
530 0         0 my $opts = $self->_get_args_as_hash( @_ );
531 0         0 $opts->{merge} = 1;
532 0         0 return( $self->sync( $opts ) );
533             }
534              
535             sub meta
536             {
537 75     75 1 1145 my $self = shift( @_ );
538 75 100       306 if( @_ )
539             {
540 68 100       429 if( $self->_is_hash( $_[0] ) )
    50          
    0          
541             {
542 11         244 $self->{meta} = shift( @_ );
543             }
544             elsif( scalar( @_ ) == 1 )
545             {
546 57         972 my $k = shift( @_ );
547 57         154 $k =~ tr/-/_/;
548 57         635 return( $self->{meta}->{ lc( $k ) } );
549             }
550             elsif( !( @_ % 2 ) )
551             {
552 0         0 my $this = { @_ };
553 0         0 foreach my $k ( keys( %$this ) )
554             {
555 0         0 my $k2 = $k;
556 0         0 $k2 =~ tr/-/_/;
557 0         0 $self->{meta}->{ lc( $k2 ) } = $this->{ $k };
558             }
559             }
560             else
561             {
562 0         0 return( $self->error( "Unknown data provided: '", join( "', '", @_ ), "'." ) );
563             }
564              
565 11         25 foreach my $k ( keys( %{$self->{meta}} ) )
  11         110  
566             {
567 48 100       219 if( CORE::index( $k, '-' ) != -1 )
568             {
569 38         128 my $k2 = $k;
570 38         69 $k2 =~ tr/-/_/;
571 38         215 $self->{meta}->{ lc( $k2 ) } = CORE::delete( $self->{meta}->{ $k } );
572             }
573             }
574             }
575 18         194 return( $self->_set_get_hash_as_mix_object( 'meta' ) );
576             }
577              
578             sub meta_keys
579             {
580 4     4 1 50672 my $self = shift( @_ );
581 4 100       24 if( @_ )
582             {
583 1         3 my $ref = shift( @_ );
584 1 50       15 return( $self->error( "Value provided is not an array reference." ) ) if( !$self->_is_array( $ref ) );
585 1         15 my $copy = [@$ref];
586 1         2 for( @$copy )
587             {
588 1         4 tr/-/_/;
589 1         4 $_ = lc( $_ );
590             }
591 1         3 $self->{meta_keys} = $copy;
592             }
593 4         16 my $data = $self->{meta_keys};
594 4 50       15 $data = [sort( keys( %{$self->{meta}} ) )] if( !scalar( @$data ) );
  0         0  
595 4         12 my $new = [];
596 4         16 for( @$data )
597             {
598 24         93 push( @$new, $self->normalise_meta( $_ ) );
599             }
600 4         65 return( $self->new_array( $new ) );
601             }
602              
603 2     2 1 53314 sub mime_version { return( shift->_set_get_meta_value( 'MIME-Version', @_ ) ); }
604              
605             sub new_element
606             {
607 9     9 1 15 my $self = shift( @_ );
608 9         96 my $opts = $self->_get_args_as_hash( @_ );
609 9         6751 $opts->{po} = $self;
610 9         50 my $e = Text::PO::Element->new( $opts );
611 9 50 33     67 $e->encoding( $self->encoding ) if( !$opts->{encoding} && $self->encoding );
612 9         5503 $e->debug( $self->debug );
613 9         272 return( $e );
614             }
615              
616             sub normalise_meta
617             {
618 47     47 1 1181 my $self = shift( @_ );
619 47   50     153 my $str = shift( @_ ) || return( '' );
620 47         86 $str =~ tr/_/-/;
621 47         1959 my @res = grep( /^$str$/i, @META );
622 47 50       172 if( scalar( @res ) )
623             {
624 47         176 return( $res[0] );
625             }
626 0         0 return( '' );
627             }
628              
629             sub parse
630             {
631 15     15 1 401296 my $self = shift( @_ );
632 15   50     132 my $this = shift( @_ ) || return( $self->error( "No file or glob was provided to parse po file." ) );
633 15         256 my $opts = $self->_get_args_as_hash( @_ );
634 15         7007 my $io;
635 15         75 my $fh_was_open = 0;
636 15 50       146 if( Scalar::Util::reftype( $this ) eq 'GLOB' )
    50          
637             {
638 0         0 $io = $this;
639 0 0       0 return( $self->error( "Filehandle provided '$io' is not opened" ) ) if( !Scalar::Util::openhandle( $io ) );
640 0         0 $fh_was_open++;
641 0         0 $self->source({ handle => $this });
642             }
643             elsif( index( $this, "\n" ) != -1 )
644             {
645 0         0 return( $self->error( "Use parse_data() if you want to parse lines of data." ) );
646             }
647             else
648             {
649             # Use the inherited method 'new_file' from Module::Generic to get a Module::Generic::File object
650 15   50     415 my $file = $self->new_file( $this ) ||
651             return( $self->pass_error );
652 15   50     2429339 $io = $file->open( '<' ) || return( $self->error( "Unable to open po file \"$this\" in read mode: $!" ) );
653             # By default
654 15         83535 $self->source({ file => $file });
655             }
656 15         94592 $io->binmode( ':utf8' );
657 15         4199 my $elem = [];
658 15         74 $self->{elements} = $elem;
659 15         87 my $header = '';
660 15         41 my $ignoring_leading_blanks = 1;
661 15         33 my $n = 0;
662              
663 15 100       181 my $include = exists( $opts->{include} ) ? $opts->{include} : $self->include;
664             # For include recursion
665 15   100     7432 my $seen_inc = $opts->{seen} // {};
666 15   100     122 my $depth = $opts->{depth} // 0;
667 15 100       127 my $max_recurse = exists( $opts->{max_recurse} ) ? $opts->{max_recurse} : $self->max_recurse;
668 15         326915 my $lang;
669              
670 15         353 my $e = Text::PO::Element->new( po => $self );
671 15         192 $e->debug( $self->debug );
672             # What was the last seen element?
673             # This is used for multi line buffer, so we know where to add it
674 15         675 my $lastSeen = '';
675 15         61 my $foundFirstLine = 0;
676             # To keep track of the msgid found so we can skip duplicates
677 15         29 my $seen = {};
678              
679             my $include_file = sub
680             {
681 9   50 9   86 my $inc_name = shift( @_ ) || return( $self->error( "No file to include was provided." ) );
682 9         24 my $c = shift( @_ ); # The original line
683 9         15 my $inc_file;
684             # Resolve path relative to current source file, if any
685 9         72 my $source = $self->source;
686 9 50 33     1650 if( $source && $source->file )
687             {
688 9   50     8293 my $base_file = $self->new_file( $source->file ) ||
689             return( $self->pass_error );
690 9   50     1516030 $inc_file = $self->new_file( $inc_name, base_file => $base_file ) ||
691             return( $self->pass_error );
692             }
693             else
694             {
695 0         0 $inc_file = $self->new_file( $inc_name );
696             }
697              
698 9 100       1285423 if( !$inc_file->exists )
699             {
700             # Add it as a comment so the user sees it
701 1         150 $e->add_comment( $c );
702 1         10 my $msg = "Include file $inc_name ($inc_file) does not exist at line $n";
703 1 50       57 warn( $msg ) if( $self->_is_warnings_enabled );
704             # Add a comment so translators see the problem:
705 1         458 $e->add_comment( "ERROR: $msg" );
706 1         16 return(1);
707             }
708              
709             # Cycle detection: avoid infinite mutual includes
710 8 100       882 if( exists( $seen_inc->{ "$inc_file" } ) )
711             {
712 1         40 my $from = $seen_inc->{ "$inc_file" };
713 1         26 my $msg = "Include file \"$inc_file\" has already been included in \"$from\".";
714 1 50       87 warn( $msg ) if( $self->_is_warnings_enabled );
715             # Optionally annotate:
716 1         449 $e->add_comment( "INFO: $msg" );
717 1         17 return(1);
718             }
719              
720             # Mark as seen from this file
721             # $this might be a glob, but the point here is to mark this include as being already processed.
722 7 50 33     587 $seen_inc->{ "$inc_file" } = ( $source && $source->file ) ? $source->file : "$this";
723              
724 7 100       13231 if( ( $depth + 1 ) > $max_recurse )
725             {
726 1 50       252 warn( "Maximum include recursion depth ($max_recurse) exceeded (", ( $depth + 1 ), "). Not parsing $inc_file" ) if( $self->_is_warnings_enabled );
727 1         567 return(1);
728             }
729              
730             # Parse include in a fresh Text::PO object
731 6         1366 my $sub = $self->new;
732 6         105 $sub->debug( $self->debug );
733              
734 6         460 my $me = $sub->parse(
735             $inc_file,
736             include => $include,
737             seen => $seen_inc,
738             depth => ( $depth + 1 ),
739             max_recurse => $max_recurse,
740             );
741 6 50       6272 if( !$me )
742             {
743 0         0 return( $self->pass_error( $sub->error ) );
744             }
745              
746             # If the include file has some meta information that include language and we do too, we check, and warn if they do not match
747 6         49 my $sub_lang = $sub->language;
748 6 50 100     208 if( defined( $lang ) && defined( $sub_lang ) && lc( $lang ) ne lc( $sub_lang ) )
      66        
749             {
750 0 0       0 warn( "Warning only: the language ($sub_lang) of the include file ($inc_file) is different than ours ($lang)" ) if( $self->_is_warnings_enabled );
751             }
752              
753 6         40 my $sub_elems = $sub->elements;
754 6 100 66     12084 if( $sub_elems && @$sub_elems )
755             {
756             # Reuse the same %$seen as for top-level duplicates
757 5         53 foreach my $se ( @$sub_elems )
758             {
759 9   100     50 my $id = $se->id // '';
760             # Skip duplicate msgid/context combos
761 9 50       58 next if( ++$seen->{ $id } > 1 );
762             # Before we add it to our elements, we change ownership
763 9         33 $se->po( $self );
764 9         2654 push( @$elem, $se );
765             }
766             }
767 6         80 return( $sub );
768 15         423 };
769              
770             # Ignore / remove possible leading blank lines
771 15         204 while( defined( $_ = $io->getline ) )
772             {
773 42         209932 $n++;
774 42 100 33     513 if( /^\S+/ )
    50          
775             {
776 29         83 $ignoring_leading_blanks = 0;
777             }
778             elsif( $ignoring_leading_blanks && /^[[:blank:]\h]*$/ )
779             {
780 0         0 next;
781             }
782             #( 1 .. /^[^\#]+$/ ) or last;
783 42 100       275 /^\#+/ || last;
784 27 100       465 if( /^\#+[[:blank:]]*(?:\.[[:blank:]]*)?\$include[[:blank:]]+(["'])(.+?)\1$/i )
    100          
785             {
786 6         53 my $inc_name = $2;
787 6 50       51 if( $include )
788             {
789 6 50       112 $include_file->( $inc_name, $_, $n ) || return( $self->pass_error );
790             }
791             # otherwise, we don't do anything, and discard the line
792             }
793             elsif( /^\#+[[:blank:]\h]*domain[[:blank:]]+\"([^\"]+)\"/ )
794             {
795 2         41 $self->domain( $1 );
796             }
797             else
798             {
799 19         139 $header .= $_;
800             }
801             }
802             # Remove trailing blank lines from header
803 15         228 $header =~ s/(^[[:blank:]\h]*\#[[:blank:]\h]*\n$)+\Z//gms;
804             # Make sure to position ourself after the initial blank line if any, since blank lines are used as separators
805             # Actually, no we don't care. Blocks are: maybe some comments, msgid then msgstr. That's how we delimit them
806             # $_ = $io->getline while( /^[[:blank:]]*$/ && defined( $_ ) );
807 15 100       284 $self->header( [split( /\n/, $header )] ) if( length( $header ) );
808              
809 15         30005 while( defined( $_ = $io->getline ) )
810             {
811 228         110992 $n++;
812 228         586 chomp( $_ );
813 228 100 66     890 if( !$foundFirstLine && /^\S/ )
814             {
815 15         56 $foundFirstLine++;
816             }
817 228 100       4228 if( /^[[:blank:]]*$/ )
    50          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
818             {
819 47 50       130 if( $foundFirstLine )
820             {
821             # Case where msgid and msgstr are separated by a blank line
822 47 50 100     669 if( scalar( @$elem ) > 1 &&
    100 100        
      66        
      66        
823             !length( $e->msgid_as_text ) &&
824             length( $e->msgstr_as_text ) &&
825             length( $elem->[-1]->msgid_as_text ) &&
826             !length( $elem->[-1]->msgstr_as_text ) )
827             {
828 0         0 $elem->[-1]->merge( $e );
829             }
830             elsif( $e->is_include )
831             {
832 1         1009 push( @$elem, $e );
833             }
834             else
835             {
836 46 100 100     43552 if( ++$seen->{ $e->id // '' } > 1 )
    100 100        
837             {
838 6         43 next;
839             }
840             elsif( !$e->id && !length( $e->msgstr // '' ) )
841             {
842             # Skipping empty first element. Probably from a bad file...
843             }
844             else
845             {
846 39         131 push( @$elem, $e );
847             }
848             }
849 41         243 $e = Text::PO::Element->new( po => $self );
850 41         1036 $e->{_po_line} = $n;
851 41 100       265 $e->encoding( $self->encoding ) if( $self->encoding );
852 41         40988 $e->debug( $self->debug );
853             }
854              
855             # special treatment for first item that contains the meta information
856 41 100       2114 if( scalar( @$elem ) == 1 )
857             {
858 10         26 my $this = $elem->[0];
859 10   50     45 my $def = $this->msgstr || [];
860 10         208 $def = [split( /\n/, join( '', @$def ) )];
861              
862 10         30 my $meta = {};
863 10         66 foreach my $s ( @$def )
864             {
865 47         92 chomp( $s );
866 47 50       340 if( $s =~ /^([^\x00-\x1f\x80-\xff :=]+):[[:blank:]]*(.*?)$/ )
867             {
868 47         259 my( $k, $v ) = ( lc( $1 ), $2 );
869 47         224 $meta->{ $k } = $v;
870 47         71 push( @{$self->{meta_keys}}, $k );
  47         160  
871 47 100       151 if( $k eq 'content-type' )
872             {
873 3 50       51 if( $v =~ /\bcharset=\s*([-\w]+)/i )
874             {
875             # my $enc = lc( $1 );
876 3         12 my $enc = $1;
877             ## See PerlIO::encoding man page
878 3 50       14 $enc = 'utf8' if( lc( $enc ) eq 'utf-8' );
879 3         14 $self->encoding( $enc );
880             # try-catch
881 3         3511 local $@;
882             eval
883 3         13 {
884 3 50       39 $io->binmode( $enc eq 'utf8' ? ":$enc" : ":encoding($enc)" );
885             };
886 3 50       1075 if( $@ )
887             {
888 0         0 return( $self->error( "Unable to set binmode to charset \"$enc\": $@" ) );
889             }
890             }
891             }
892             }
893             }
894 10 50       38 if( scalar( keys( %$meta ) ) )
895             {
896 10 50 33     192 $lang = $meta->{language} if( exists( $meta->{language} ) && defined( $meta->{language} ) && length( $meta->{language} // '' ) );
      50        
      33        
897 10         90 $self->meta( $meta );
898 10         455884 $this->is_meta(1);
899             }
900             }
901             }
902             # #. TRANSLATORS: A test phrase with all letters of the English alphabet.
903             # #. Replace it with a sample text in your language, such that it is
904             # #. representative of language's writing system.
905             # We make sure this is not confused with a non-standard include directive
906             elsif( /^\#\.[[:blank:]]*(?<text>(?!.*\$include[[:blank:]]+["'][^"']*["']).*?)$/i )
907             {
908 0         0 my $c = $1;
909 0         0 $e->add_auto_comment( $c );
910             }
911             # #: finddialog.cpp:38
912             # #: colorscheme.cpp:79 skycomponents/equator.cpp:31
913             elsif( /^\#\:[[:blank:]]+(.*?)$/ )
914             {
915 9         36 my $c = $1;
916 9         46 $e->reference( $c );
917             }
918             # #, c-format
919             elsif( /^\#\,[[:blank:]]+(.*?)$/ )
920             {
921 0         0 my $c = $1;
922 0 0       0 $e->flags( [ split( /[[:blank:]]*,[[:blank:]]*/, $c ) ] ) if( $c );
923             }
924             # Some other comments:
925             # - domain declaration
926             # - auto comment (extracted with xgettext from the code)
927             # - $include directives
928             elsif( /^\#+(.*?)$/ )
929             {
930 6         41 my $c = $1;
931              
932             # NOTE: Include directive:
933             # # $include "file.po"
934             # #. $include "file.po"
935             # #.$include "file.po"
936             # case insensitive, and single or double quote is ok.
937 6 100       59 if( $c =~ /^(?:(?:\.[[:blank:]]*)|[[:blank:]]+)\$include[[:blank:]]+(["'])(.+?)\1/i )
938             {
939 4         27 my $inc_name = $2;
940 4 100       31 if( $include )
941             {
942 3 50       47 $include_file->( $inc_name, $c ) || return( $self->pass_error );
943              
944             # Since this line is an include directive, we do not treat it as a normal comment.
945 3         72710 next;
946             }
947             # We just record it
948             else
949             {
950 1         32 $e->is_include(1);
951 1         1261 $e->file( $inc_name );
952             }
953             }
954              
955             # Normal comment / domain handling as before
956 3 50 33     1166 if( !$self->meta->length && $c =~ /^domain[[:blank:]\h]+\"(.*?)\"/ )
    100          
957             {
958 0         0 $self->domain( $1 );
959             }
960             # It could be a blank auto comment, but we keep it to represent faithfully what we found.
961             elsif( $c =~ /^\.[[:blank:]]*(.*?)$/ )
962             {
963 1         41220 my $auto_comment = $1;
964             # Trim leading and trailing spaces
965 1         10 $auto_comment =~ s/^[[:blank:]]+|[[:blank:]]+$//g;
966 1         15 $e->add_auto_comment( $auto_comment );
967             }
968             else
969             {
970             # Trim leading and trailing spaces
971 2         82947 $c =~ s/^[[:blank:]]+|[[:blank:]]+$//g;
972 2         19 $e->add_comment( $c );
973             }
974             }
975             elsif( /^msgid[[:blank:]]+"(.*?)"$/ )
976             {
977 44 100       337 $e->msgid( $self->unquote( $1 ) ) if( length( $1 ) );
978 44         7646 $lastSeen = 'msgid';
979             }
980             # #: mainwindow.cpp:127
981             # #, kde-format
982             # msgid "Time: %1 second"
983             # msgid_plural "Time: %1 seconds"
984             # msgstr[0] "Tiempo: %1 segundo"
985             # msgstr[1] "Tiempo: %1 segundos"
986             elsif( /^msgid_plural[[:blank:]]+"(.*?)"[[:blank:]]*$/ )
987             {
988 3 50       25 $e->msgid_plural( $self->unquote( $1 ) ) if( length( $1 ) );
989 3         884 $e->plural(1);
990 3         3727 $lastSeen = 'msgid_plural';
991             }
992             # disambiguating context:
993             # #: tools/observinglist.cpp:700
994             # msgctxt "First letter in 'Scope'"
995             # msgid "S"
996             # msgstr ""
997             #
998             # #: skycomponents/horizoncomponent.cpp:429
999             # msgctxt "South"
1000             # msgid "S"
1001             # msgstr ""
1002             elsif( /^msgctxt[[:blank:]]+"(.*?)"[[:blank:]]*$/ )
1003             {
1004 2 50       27 $e->context( $self->unquote( $1 ) ) if( length( $1 ) );
1005 2         2417 $lastSeen = 'msgctxt';
1006             }
1007             elsif( /^msgstr[[:blank:]]+"(.*?)"[[:blank:]]*$/ )
1008             {
1009 42 100       219 $e->msgstr( $self->unquote( $1 ) ) if( length( $1 ) );
1010 42         174 $lastSeen = 'msgstr';
1011             }
1012             elsif( /^msgstr\[(\d+)\][[:blank:]]+"(.*?)"[[:blank:]]*$/ )
1013             {
1014 8 50       43 if( length( $2 ) )
1015             {
1016 8         34 $e->msgstr( $1, $self->unquote( $2 ) );
1017 8         29 $e->plural(1);
1018             }
1019 8         9905 $lastSeen = 'msgstr';
1020             }
1021             elsif( /^[[:blank:]]*"(.*?)"[[:blank:]]*$/ )
1022             {
1023 65         222 my $sub = "add_${lastSeen}";
1024 65 50       708 if( $e->can( $sub ) )
1025             {
1026 65 50       526 $e->$sub( $self->unquote( $1 ) ) if( length( $1 ) );
1027             }
1028             else
1029             {
1030 0         0 warn( "Unable to find method \"${sub}\" in class \"", ref( $e ), "\" for line parsed \"$_\"\n" );
1031             }
1032             }
1033             elsif( /^\#[[:blank:]\h]*$/ )
1034             {
1035             # Found some standalone comments; we just ignore
1036             }
1037             else
1038             {
1039 2 50       31 warn( "I do not understand the line \"$_\" at line $n\n" ) if( $self->_is_warnings_enabled );
1040             }
1041             }
1042 15 50       5032 $io->close unless( $fh_was_open );
1043 15 100 50     34406 if( scalar( @$elem ) )
    50          
1044             {
1045 14 100 66     204 if( $elem->[-1] ne $e &&
      100        
1046             CORE::length( $e->msgid_as_text ) &&
1047             ++$seen->{ $e->msgid_as_text } < 2 )
1048             {
1049 3         11 push( @$elem, $e );
1050             }
1051 14 100       67 shift( @$elem ) if( $elem->[0]->is_meta );
1052             }
1053             elsif( $e->msgid // '' )
1054             {
1055 0         0 push( @$elem, $e );
1056             }
1057             # Mark this instance as having parsed data (vs an instance where we build data programmatically)
1058 15         13307 $self->{_parsed} = 1;
1059 15         9701 return( $self );
1060             }
1061              
1062             sub parse_date_to_object
1063             {
1064 4     4 1 37 my $self = shift( @_ );
1065 4         13 my $str = shift( @_ );
1066 4   50     124 my $d = $self->_parse_timestamp( $str ) ||
1067             return( $self->error( "Date time string provided is unsupported: \"${str}\"." ) );
1068 4         961367 my $strp = $d->formatter;
1069 4 50       28 unless( $strp )
1070             {
1071 0         0 $strp = DateTime::Format::Strptime->new(
1072             pattern => '%Y-%m-%d %H:%M%z',
1073             locale => 'en_GB',
1074             time_zone => $d->time_zone,
1075             );
1076 0         0 $d->set_formatter( $strp );
1077             }
1078 4         116 return( $d );
1079             }
1080              
1081             sub parse_header_value
1082             {
1083 2     2 1 6 my $self = shift( @_ );
1084 2         3 my $s = shift( @_ );
1085 2 50 33     27 return( $self->error( 'Argument string is required' ) ) if( !defined( $s ) || !length( $s ) );
1086 2 50       9 my $sep = @_ ? shift( @_ ) : ';';
1087 2         4 my @parts = ();
1088 2         4 my $i = 0;
1089 2         115 foreach( split( /(\\.)|$sep/, $s ) )
1090             {
1091 6 100       20 defined( $_ ) ? do{ $parts[$i] .= $_ } : do{ $i++ };
  4         12  
  2         4  
1092             }
1093 2         7 my $header_val = shift( @parts );
1094 2         69 my $obj = Text::PO::HeaderValue->new( $header_val );
1095              
1096 2         22 my $param = {};
1097 2         7 foreach my $frag ( @parts )
1098             {
1099 2         21 $frag =~ s/^[[:blank:]]+|[[:blank:]]+$//g;
1100 2         24 my( $attribute, $value ) = split( /[[:blank:]]*\=[[:blank:]]*/, $frag, 2 );
1101 2         14 $value =~ s/^\"|\"$//g;
1102             # Check character string and length. Should not be more than 255 characters
1103             # http://tools.ietf.org/html/rfc1341
1104             # http://www.iana.org/assignments/media-types/media-types.xhtml
1105             # Won't complain if this does not meet our requirement, but will discard it silently
1106 2 50 33     66 if( $attribute =~ /^[a-zA-Z][a-zA-Z0-9\_\-]+$/ && CORE::length( $attribute ) <= 255 )
1107             {
1108 2 50 33     30 if( $value =~ /^[a-zA-Z][a-zA-Z0-9\_\-]+$/ && CORE::length( $value ) <= 255 )
1109             {
1110 2         17 $obj->param( lc( $attribute ) => $value );
1111             }
1112             }
1113             }
1114 2         8 return( $obj );
1115             }
1116              
1117             sub parse2hash
1118             {
1119 0     0 1 0 my $self = shift( @_ );
1120 0   0     0 my $this = shift( @_ ) || return( $self->error( "No file or glob was provided to parse po file." ) );
1121 0         0 my $buff = '';
1122 0 0 0     0 if( $self->{use_json} && ( -e( "${this}.json" ) || $this =~ /\.json$/ ) )
      0        
1123             {
1124 0 0       0 my $file = -e( "${this}.json" ) ? "${this}.json" : $this;
1125 0   0     0 my $io = IO::File->new( "$file" ) || return( $self->error( "Unable to open json po file \"${file}\" in read mode: $!" ) );
1126 0         0 $io->binmode( ':utf8' );
1127 0         0 $io->read( $buff, -s( $file ) );
1128 0         0 $io->close;
1129 0         0 my $j = JSON->new->relaxed;
1130 0         0 my $ref = {};
1131             # try-catch
1132 0         0 local $@;
1133             eval
1134 0         0 {
1135 0         0 $ref = $j->decode( $buff );
1136             };
1137 0 0       0 if( $@ )
1138             {
1139 0         0 return( $self->error( "An error occurred while json decoding data from \"${file}\": $@" ) );
1140             }
1141 0         0 my $hash = {};
1142 0         0 foreach my $elem ( @{$ref->{elements}} )
  0         0  
1143             {
1144 0         0 $hash->{ $elem->{msgid} } = $elem->{msgstr};
1145             }
1146 0         0 return( $self->new_hash( $hash ) );
1147             }
1148             else
1149             {
1150 0 0       0 $self->parse( $this ) || return( $self->pass_error );
1151 0         0 return( $self->hash );
1152             }
1153             }
1154              
1155             sub parse2object
1156             {
1157 2     2 1 11 my $self = shift( @_ );
1158 2   50     14 my $this = shift( @_ ) || return( $self->error( "No file or glob was provided to parse po file." ) );
1159 2         26 my $buff = '';
1160 2 50 33     10 if( $self->{use_json} && ( -e( "${this}.json" ) || $this =~ /\.json$/ ) )
      33        
1161             {
1162 2 50       359 my $file = -e( "${this}.json" ) ? "${this}.json" : $this;
1163 2   50     114 my $io = IO::File->new( $file ) || return( $self->error( "Unable to open json po file \"${file}\" in read mode: $!" ) );
1164 2         648 $io->binmode( ':utf8' );
1165 2         53 $io->read( $buff, -s( $file ) );
1166 2         470 $io->close;
1167 2         124 my $j = JSON->new->relaxed;
1168 2         12 my $ref = {};
1169             # try-catch
1170 2         7 local $@;
1171             eval
1172 2         8 {
1173 2         251 $ref = $j->decode( $buff );
1174             };
1175 2 50       14 if( $@ )
1176             {
1177 0         0 return( $self->error( "An error occurred while json decoding data from \"${file}\": $@" ) );
1178             }
1179              
1180 2 50 33     24 $self->domain( $ref->{domain} ) if( length( $ref->{domain} ) && !length( $self->domain ) );
1181 2         2036 my $meta_keys = [];
1182 2 50       10 if( $ref->{meta_keys} )
    0          
1183             {
1184 2         6 $meta_keys = $ref->{meta_keys};
1185             }
1186             elsif( $ref->{meta} )
1187             {
1188 0         0 $meta_keys = [sort( keys( %{$ref->{meta}} ) )];
  0         0  
1189             }
1190              
1191 2 50       14 if( $ref->{meta} )
1192             {
1193 2         7 $self->{meta} = {};
1194 2         3 foreach my $k ( keys( %{$ref->{meta}} ) )
  2         30  
1195             {
1196 22         41 my $k2 = lc( $k );
1197 22         29 $k2 =~ tr/-/_/;
1198 22         116 $self->{meta}->{ $k2 } = $ref->{meta}->{ $k };
1199             }
1200             }
1201 2         7 $self->{meta_keys} = $meta_keys;
1202              
1203 2 50       12 if( scalar( @$meta_keys ) )
1204             {
1205 2         46 my $e = Text::PO::Element->new( 'po' => $self );
1206 2         17 $e->debug( $self->debug );
1207 2         100 $e->msgid( '' );
1208             $e->msgstr(
1209 2         638 [map( sprintf( '%s: %s', $_, $ref->{meta}->{ $_ } ), @$meta_keys )]
1210             );
1211 2         16 $e->is_meta(1);
1212 2         2326 push( @{$self->{elements}}, $e );
  2         10  
1213             }
1214              
1215 2         16 foreach my $def ( @{$ref->{elements}} )
  2         7  
1216             {
1217 16         83 my $e = Text::PO::Element->new( 'po' => $self );
1218 16         136 $e->debug( $self->debug );
1219 16         681 $e->msgid( $def->{msgid} );
1220 16 100       3664 if( $def->{msgid_plural} )
1221             {
1222 2         13 $e->msgid_plural( $def->{msgid_plural} );
1223             }
1224 16 100       518 if( ref( $def->{msgstr} ) eq 'ARRAY' )
1225             {
1226 2         6 for( my $i = 0; $i < scalar( @{$def->{msgstr}} ); $i++ )
  5         20  
1227             {
1228 3         12 $e->msgstr( $i => $def->{msgstr}->[$i] );
1229             }
1230             }
1231             else
1232             {
1233 14         45 $e->msgstr( $def->{msgstr} );
1234             }
1235 16 50       38 $e->comment( $def->{comment} ) if( $def->{comment} );
1236 16 50       34 $e->context( $def->{context} ) if( $def->{context} );
1237 16 50       31 $e->flags( $def->{flags} ) if( $def->{flags} );
1238 16 100       56 $e->reference( $def->{reference} ) if( $def->{reference} );
1239 16 50       60 $e->encoding( $self->encoding ) if( $self->encoding );
1240 16         14308 push( @{$self->{elements}}, $e );
  16         66  
1241             }
1242 2         125 return( $self );
1243             }
1244             else
1245             {
1246 0         0 return( $self->parse( $this ) );
1247             }
1248             }
1249              
1250             sub plural
1251             {
1252 2     2 1 7 my $self = shift( @_ );
1253 2 50       10 if( @_ )
1254             {
1255 0         0 my( $nplurals, $expr ) = @_;
1256 0         0 $self->{plural} = [ $nplurals, $expr ];
1257 0         0 return( [ @{$self->{plural}} ] );
  0         0  
1258             }
1259             else
1260             {
1261 2 0 50     9 return( [@{$self->{plural}}] ) if( $self->{plural} && scalar( @{$self->{plural}} ) );
  0         0  
  0         0  
1262 2         9 my $meta = $self->meta;
1263 2         171284 my $pluralDef = $self->meta( 'Plural-Forms' );
1264 2 50       91 if( $pluralDef )
1265             {
1266 2 50       51 if( $pluralDef =~ /^[[:blank:]\h]*nplurals[[:blank:]\h]*=[[:blank:]\h]*(\d+)[[:blank:]\h]*\;[[:blank:]\h]*plural[[:blank:]\h]*=[[:blank:]\h]*(.*?)\;?$/i )
1267             {
1268 2         19 $self->{plural} = [ $1, $2 ];
1269 2         20 return( $self->{plural} );
1270             }
1271             else
1272             {
1273 0         0 return( $self->error( "Malformed plural definition found in po data in meta field \"Plural-Forms\": " . $pluralDef ) );
1274             }
1275             }
1276 0         0 return( [] );
1277             }
1278             }
1279              
1280 3     3 1 2843 sub plural_forms { return( shift->_set_get_meta_value( 'Plural-Forms', @_ ) ); }
1281              
1282 3     3 1 1716 sub po_revision_date { return( shift->_set_get_meta_date( 'PO-Revision-Date', @_ ) ); }
1283              
1284 1     1 1 10 sub pot_creation_date { return( shift->_set_get_meta_date( 'POT-Creation-Date', @_ ) ); }
1285              
1286 2     2 1 12 sub project_id_version { return( shift->_set_get_meta_value( 'Project-Id-Version', @_ ) ); }
1287              
1288 2     2 1 573 sub report_bugs_to { return( shift->_set_get_meta_value( 'Report-Msgid-Bugs-To', @_ ) ); }
1289              
1290             sub quote
1291             {
1292 21     21 1 610 my $self = shift( @_ );
1293 21         38 my $str = shift( @_ );
1294 21 50       57 return( '' ) if( !length( $str ) );
1295             # \t is a tab
1296 21         63 $str =~ s/(?<!\\)\\(?!t)/\\\\/g;
1297 21         36 $str =~ s/(?<!\\)"/\\"/g;
1298 21         32 $str =~ s/(?<!\\)\n/\\n/g;
1299 21         113 return( sprintf( '%s', $str ) );
1300             }
1301              
1302 0     0 1 0 sub remove_duplicates { return( shift->_set_get_boolean( 'remove_duplicates', @_ ) ); }
1303              
1304             sub remove_element
1305             {
1306 0     0 1 0 my $self = shift( @_ );
1307 0         0 my $elem = shift( @_ );
1308 0         0 my $rv = $self->exists( $elem );
1309 0 0       0 return if( !defined( $rv ) );
1310 0 0       0 return(0) if( !$rv );
1311 0         0 my $elems = $self->elements;
1312 0         0 my $found = 0;
1313 0         0 for( my $i = 0; $i < scalar( @$elems ); $i++ )
1314             {
1315 0 0       0 if( $elems->[$i] eq $elem )
1316             {
1317 0         0 splice( @$elems, $i, 1 );
1318 0         0 $i--;
1319 0         0 $found++;
1320             }
1321             }
1322 0         0 return( $found );
1323             }
1324              
1325 0     0 1 0 sub removed { return( shift->_set_get_array_as_object( 'removed', @_ ) ); }
1326              
1327             sub set_default_meta
1328             {
1329 0     0 1 0 my $self = shift( @_ );
1330 0         0 foreach my $k ( @META )
1331             {
1332 0         0 my $k2 = lc( $k );
1333 0         0 $k2 =~ tr/-/_/;
1334 0 0 0     0 if( !exists( $self->{meta}->{ $k2 } ) &&
1335             length( $DEF_META->{ $k } ) )
1336             {
1337 0         0 $self->{meta}->{ $k2 } = $DEF_META->{ $k };
1338             }
1339             }
1340 0         0 return( $self );
1341             }
1342              
1343 24     24 1 335 sub source { return( shift->_set_get_hash_as_object( 'source', @_ ) ); }
1344              
1345             sub sync
1346             {
1347 0     0 1 0 my $self = shift( @_ );
1348             # a filehandle, or a filename?
1349             # my $this = shift( @_ ) || return( $self->error( "No file or filehandle provided." ) );
1350 0         0 my $this;
1351 0 0 0     0 $this = shift( @_ ) if( scalar( @_ ) && ( ( @_ % 2 ) || ( !( @_ % 2 ) && ref( $_[1] ) eq 'HASH' ) ) );
      0        
1352 0         0 my $opts = $self->_get_args_as_hash( @_ );
1353 0 0 0     0 $this = ( $opts->{handle} || $opts->{file} ) if( !CORE::length( $this ) );
1354 0 0       0 if( !$this )
1355             {
1356 0         0 my $fh;
1357 0 0       0 if( $fh = $self->source->handle )
    0          
1358             {
1359 0 0       0 $this = $fh if( $self->_can_write_fh( $fh ) );
1360             }
1361             elsif( my $file = $self->source->file )
1362             {
1363 0 0 0     0 $this = $file if( -e( $file ) && -w( $file ) );
1364 0   0     0 $fh = IO::File->new( ">$file" ) || return( $self->error( "Unable to open file \"$file\" in write mode: $!" ) );
1365             }
1366 0 0       0 return( $self->error( "No writable file handle or file set to sync our data against." ) ) if( !$this );
1367 0         0 $fh->binmode( ':utf8' );
1368 0 0       0 $self->dump( $fh ) || return( $self->pass_error );
1369 0         0 $fh->close;
1370 0         0 return( $self );
1371             }
1372              
1373 0 0       0 if( Scalar::Util::reftype( $this ) eq 'GLOB' )
    0          
    0          
1374             {
1375 0 0       0 return( $self->error( "Filehandle provided is not opened" ) ) if( !Scalar::Util::openhandle( $this ) );
1376 0 0       0 return( $self->error( "Filehandle provided is not writable" ) ) if( !$self->_can_write_fh( $this ) );
1377 0         0 return( $self->sync_fh( $this, $opts ) );
1378             }
1379             elsif( -l( $this ) )
1380             {
1381 0         0 return( $self->error( "File provided is actually a symbolic link. Do not want to write to a symbolic link." ) );
1382             }
1383             elsif( -e( $this ) )
1384             {
1385 0 0       0 if( !-f( $this ) )
1386             {
1387 0         0 return( $self->error( "File '$this' is not a file." ) );
1388             }
1389 0   0     0 my $fh = IO::File->new( "+<$this" ) || return( $self->error( "Unable to open file '$this' in read/write mode: $!" ) );
1390 0         0 my $po = $self->sync_fh( $fh, $opts );
1391 0         0 $fh->close;
1392 0         0 return( $po );
1393             }
1394             # Does not exist yet
1395             else
1396             {
1397 0   0     0 my $fh = IO::File->new( ">$this" ) || return( $self->error( "Unable to write to file '$this': $!" ) );
1398 0 0       0 $self->dump( $fh ) || return( $self->pass_error );
1399 0         0 $fh->close;
1400             }
1401 0         0 return( $self );
1402             }
1403              
1404             sub sync_fh
1405             {
1406 0     0 1 0 my $self = shift( @_ );
1407 0         0 my $fh = shift( @_ );
1408 0 0       0 return( $self->error( "Filehandle provided $fh is not a valid file handle" ) ) if( !Scalar::Util::openhandle( $fh ) );
1409 0         0 my $opts = $self->_get_args_as_hash( @_ );
1410             # Parse file
1411 0         0 my $po = $self->new( include => 0 );
1412 0         0 $po->debug( $self->debug );
1413             # Load the target data file
1414 0         0 $po->parse( $fh );
1415             # Remove the ones that do not exist
1416 0         0 my $elems = $po->elements;
1417 0         0 my @removed = ();
1418             # Check the target elements against ours
1419 0         0 for( my $i = 0; $i < scalar( @$elems ); $i++ )
1420             {
1421 0         0 my $e = $elems->[$i];
1422             # Do we have the target element ? If not, it was removed
1423 0 0       0 if( !$self->exists( $e, { msgid_only => 1 } ) )
1424             {
1425 0         0 my $removedObj = splice( @$elems, $i, 1 );
1426 0 0       0 push( @removed, $removedObj ) if( $removedObj );
1427 0         0 $i--;
1428             }
1429             else
1430             {
1431             # Ok, already exists
1432             }
1433             }
1434             # Now check each one of ours against this parsed file and add our items if missing
1435 0         0 $elems = $self->elements;
1436 0         0 my @added = ();
1437             # Check our source elements against the target ones
1438 0         0 foreach my $e ( @$elems )
1439             {
1440             # Does the target file have our element ? If not, we add it.
1441 0 0       0 if( !$po->exists( $e, { msgid_only => 1 } ) )
1442             {
1443 0 0       0 if( $e->is_include )
1444             {
1445 0         0 $po->add_include( $e );
1446             }
1447             else
1448             {
1449 0         0 $po->add_element( $e );
1450             }
1451 0         0 push( @added, $e );
1452             }
1453             else
1454             {
1455             # Ok, already exists
1456             }
1457             }
1458             # Now, rewind and rewrite the file
1459 0 0       0 $fh->seek(0,0) || return( $self->error( "Unable to seek file handle!: $!" ) );
1460             # $fh->print( $po->dump );
1461 0 0       0 $po->dump( $fh ) || return( $self->pass_error );
1462 0         0 $fh->truncate( $fh->tell );
1463 0         0 $po->added( \@added );
1464 0         0 $po->removed( \@removed );
1465 0         0 return( $po );
1466             }
1467              
1468             sub unquote
1469             {
1470 133     133 1 295 my $self = shift( @_ );
1471 133         302 my $str = shift( @_ );
1472 133 50       331 return( '' ) if( !length( $str ) );
1473 133         334 $str =~ s/^"(.*)"/$1/;
1474 133         270 $str =~ s/\\"/"/g;
1475             ## newline
1476 133         526 $str =~ s/(?<!(\\))\\n/\n/g;
1477             ## inline newline
1478 133         302 $str =~ s/(?<!(\\))\\{2}n/\\n/g;
1479             ## \ followed by newline
1480 133         231 $str =~ s/(?<!(\\))\\{3}n/\\\n/g;
1481             ## \ followed by inline newline
1482 133         232 $str =~ s/\\{4}n/\\\\n/g;
1483             ## all slashes not related to a newline
1484 133         224 $str =~ s/\\\\(?!n)/\\/g;
1485 133         714 return( $str );
1486             }
1487              
1488 2     2 1 2323 sub use_json { return( shift->_set_get_boolean( 'use_json', @_ ) ); }
1489              
1490             # https://stackoverflow.com/questions/3807231/how-can-i-test-if-i-can-write-to-a-filehandle
1491             # -> https://stackoverflow.com/a/3807381/4814971
1492             sub _can_write_fh
1493             {
1494 0     0   0 my $self = shift( @_ );
1495 0         0 my $fh = shift( @_ );
1496 0         0 my $flags = fcntl( $fh, F_GETFL, 0 );
1497 0 0       0 if( ( $flags & O_ACCMODE ) & ( O_WRONLY|O_RDWR ) )
1498             {
1499 0         0 return(1);
1500             }
1501 0         0 return(0);
1502             }
1503              
1504             sub _set_get_meta_date
1505             {
1506 4     4   15 my $self = shift( @_ );
1507 4   50     36 my $field = shift( @_ ) || return( $self->error( "No field was provided to get its DateTime object equivalent." ) );
1508 4 50       19 if( @_ )
1509             {
1510 0         0 my $v = shift( @_ );
1511 0 0 0     0 if( ref( $v ) && $self->_is_a( $v => 'DateTime' ) )
1512             {
1513 0         0 my $strp = DateTime::Format::Strptime->new(
1514             pattern => '%F %H:%M%z',
1515             locale => 'en_GB',
1516             time_zone => ( HAS_LOCAL_TZ ? 'local' : 'UTC' ),
1517             );
1518 0         0 $v->set_formatter( $strp );
1519             }
1520 0         0 $self->meta( $field => $v );
1521 0         0 return( $v );
1522             }
1523             else
1524             {
1525 4         22 my $meta = $self->meta( $field );
1526 4 50 33     113 if( !defined( $meta ) || !length( $meta ) )
1527             {
1528 0         0 return;
1529             }
1530 4         28 return( $self->parse_date_to_object( $meta ) );
1531             }
1532             }
1533              
1534             sub _set_get_meta_value
1535             {
1536 27     27   69 my $self = shift( @_ );
1537 27   50     170 my $field = shift( @_ ) || return( $self->error( "No field was provided to get its DateTime object equivalent." ) );
1538 27 50       93 if( @_ )
1539             {
1540 0         0 my $v = shift( @_ );
1541 0         0 $self->meta( $field => $v );
1542             }
1543 27         145 return( $self->meta( $field ) );
1544             }
1545              
1546             # NOTE: Text::PO::HeaderValue class
1547             {
1548             package
1549             Text::PO::HeaderValue;
1550             BEGIN
1551             {
1552 5     5   68 use strict;
  5         10  
  5         286  
1553 5     5   42 use warnings;
  5         9  
  5         433  
1554 5     5   46 use parent qw( Module::Generic );
  5         10  
  5         54  
1555 5     5   527 use vars qw( $VERSION $QUOTE_REGEXP $TYPE_REGEXP $TOKEN_REGEXP $TEXT_REGEXP );
  5         9  
  5         643  
1556 5     5   1453 our $VERSION = 'v0.1.0';
1557             use overload (
1558 5         67 '""' => 'as_string',
1559             fallback => 1,
1560 5     5   35 );
  5         8  
1561 5         31 our $QUOTE_REGEXP = qr/([\\"])/;
1562             #
1563             # RegExp to match type in RFC 7231 sec 3.1.1.1
1564             #
1565             # media-type = type "/" subtype
1566             # type = token
1567             # subtype = token
1568             #
1569 5         395 our $TYPE_REGEXP = qr/^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
1570 5         193 our $TOKEN_REGEXP = qr/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
1571 5         202 our $TEXT_REGEXP = qr/^[\u000b\u0020-\u007e\u0080-\u00ff]+$/;
1572             };
1573              
1574 5     5   38 use strict;
  5         10  
  5         171  
1575 5     5   24 use warnings;
  5         9  
  5         4204  
1576              
1577             sub init
1578             {
1579 2     2   232 my $self = shift( @_ );
1580 2         8 my $value = shift( @_ );
1581 2 50       9 return( $self->error( "No value provided." ) ) if( !length( $value ) );
1582 2         177 $self->{original} = '';
1583 2         13 $self->{value} = $value;
1584 2         24 $self->SUPER::init( @_ );
1585 2         179 $self->{params} = {};
1586 2         7 return( $self );
1587             }
1588              
1589             sub as_string
1590             {
1591 0     0   0 my $self = shift( @_ );
1592 0 0 0     0 if( !defined( $self->{original} ) || !length( $self->{original} ) )
1593             {
1594 0         0 my $string = '';
1595 0 0 0     0 if( defined( $self->{value} ) && length( $self->{value} ) )
1596             {
1597 0 0       0 if( $self->{value} !~ /^$TYPE_REGEXP$/ )
1598             {
1599 0         0 return( $self->error( "Invalid value \"$self->{value}\"" ) );
1600             }
1601 0         0 $string = $self->{value};
1602             }
1603              
1604             # Append parameters
1605 0 0 0     0 if( $self->{params} && ref( $self->{params} ) eq 'HASH' )
1606             {
1607 0         0 my $params = [ sort( keys( %{$self->{params}} ) ) ];
  0         0  
1608 0         0 for( my $i = 0; $i < scalar( @$params ); $i++ )
1609             {
1610 0 0       0 if( $params->[$i] !~ /^$TOKEN_REGEXP$/ )
1611             {
1612 0         0 return( $self->error( "Invalid parameter name: \"" . $params->[$i] . "\"" ) );
1613             }
1614 0 0       0 if( length( $string ) > 0 )
1615             {
1616 0         0 $string .= '; ';
1617             }
1618 0         0 $string .= $params->[$i] . '=' . $self->qstring( $self->{params}->{ $params->[$i] } );
1619             }
1620             }
1621 0         0 $self->{original} = $string;
1622             }
1623 0         0 return( $self->{original} );
1624             }
1625              
1626 0     0   0 sub original { return( shift->_set_get_scalar_as_object( 'original', @_ ) ); }
1627              
1628             sub param
1629             {
1630 2     2   5 my $self = shift( @_ );
1631 2   50     9 my $name = shift( @_ ) || return( $self->error( "No parameter name was provided." ) );
1632 2 50       7 if( @_ )
1633             {
1634 2         3 my $v = shift( @_ );
1635 2         11 $self->{params}->{ $name } = $v;
1636             }
1637 2         8 return( $self->{params}->{ $name } );
1638             }
1639              
1640             sub qstring
1641             {
1642 0     0     my $self = shift( @_ );
1643 0           my $str = shift( @_ );
1644              
1645             # no need to quote tokens
1646 0 0         if( $str =~ /^$TOKEN_REGEXP$/ )
1647             {
1648 0           return( $str );
1649             }
1650              
1651 0 0 0       if( length( $str ) > 0 && $str !~ /^$TEXT_REGEXP$/ )
1652             {
1653 0           return( $self->error( 'Invalid parameter value' ) );
1654             }
1655              
1656 0           $str =~ s/$QUOTE_REGEXP/\\$1/g;
1657 0           return( '"' . $str . '"' );
1658             }
1659              
1660 0     0     sub value { return( shift->_set_get_scalar_as_object( 'value', @_ ) ); }
1661             }
1662              
1663             1;
1664             # NOTE: POD
1665             __END__
1666              
1667             =encoding utf-8
1668              
1669             =head1 NAME
1670              
1671             Text::PO - Read and write PO files
1672              
1673             =head1 SYNOPSIS
1674              
1675             use Text::PO;
1676             # Create a parser (include directives enabled by default)
1677             my $po = Text::PO->new;
1678             $po->debug(2);
1679             $po->parse( $poFile ) || die( $po->error );
1680              
1681             # Or disable include processing for this parsing
1682             $po->parse( $poFile, include => 0 );
1683              
1684             # Retrieve parsed elements
1685             my $hash = $po->as_hash;
1686             my $json = $po->as_json;
1687              
1688             # Serialize back to PO text
1689             my $str = $po->as_string;
1690              
1691             # Add data:
1692             my $e = $po->add_element(
1693             msgid => 'Hello!',
1694             msgstr => 'Salut !',
1695             );
1696             my $e = $po->add_include(
1697             file => 'include/me.po',
1698             after => 'Hello world!',
1699             );
1700              
1701             $po->remove_element( $e );
1702              
1703             # Iterate over elements
1704             $po->elements->foreach(sub
1705             {
1706             my $e = shift( @_ ); # $_ is also available
1707             if( $e->msgid_as_text eq 'Hello!' )
1708             {
1709             # do something
1710             }
1711             });
1712              
1713             Or, maybe using the object overloading directly:
1714              
1715             $po->elements->foreach(sub
1716             {
1717             my $e = shift( @_ ); # $_ is also available
1718             if( $e eq $other )
1719             {
1720             # do something
1721             }
1722             });
1723              
1724             # Write in a PO format to STDOUT
1725             $po->dump;
1726             # or to a file handle
1727             $po->dump( $io );
1728              
1729             # Synchronise data
1730             $po->sync( '/some/where/com.example.api.po' );
1731             $po->sync( $file_handle );
1732              
1733             # or merge
1734             $po->merge( '/some/where/com.example.api.po' );
1735             $po->merge( $file_handle );
1736              
1737             =head1 VERSION
1738              
1739             v0.9.1
1740              
1741             =head1 DESCRIPTION
1742              
1743             This module parse GNU PO (portable object) and POT (portable object template) files, making it possible to edit the localised text and write it back to a po file.
1744              
1745             L<Text::PO::MO> reads and writes C<.mo> (machine object) binary files.
1746              
1747             Thus, with those modules, you do not need to install C<msgfmt>, C<msginit> of GNU. It is better if you have them though.
1748              
1749             Also, this distribution provides a way to export the C<po> files in json format to be used from within JavaScript and a JavaScript class to load and use those files is also provided along with some command line scripts. See the C<share> folder along with its own test units.
1750              
1751             Also, there is a script in C<scripts> that can be used to transcode C<.po> or C<mo> files into json format and vice versa.
1752              
1753             For more information on the format of a PO element, check L<Text::PO::Element>
1754              
1755             =head1 CONSTRUCTOR
1756              
1757             =head2 new
1758              
1759             Create a new Text::PO object acting as an accessor.
1760              
1761             One object should be created per po file, because it stores internally the po data for that file in the L<Text::PO> object instantiated.
1762              
1763             Returns the object.
1764              
1765             The following options can be provided:
1766              
1767             =over 4
1768              
1769             =item * C<domain>
1770              
1771             The PO domain.
1772              
1773             =item * C<header>
1774              
1775             An array reference of PO file header string. Those are often lines of comments preceded by a pound sign (C<#>), possibly with some copyright information.
1776              
1777             =item * C<encoding>
1778              
1779             The content encoding of the file, such as C<utf-8>
1780              
1781             =item * C<include>
1782              
1783             Defaults to true.
1784              
1785             A boolean value (C<1> or C<0>) to indicate whether the parser should recognise include directives or not.
1786              
1787             =item * C<max_recurse>
1788              
1789             Defaults to 32
1790              
1791             An unsigned integer value representing the maximum recursion allowed when C<include> is enabled, and when the parser finds include directives.
1792              
1793             =item * C<meta>
1794              
1795             An hash reference of meta key-value pairs, with the keys in all lower case.
1796              
1797             =item * C<meta_keys>
1798              
1799             An array reference of meta keys found,
1800              
1801             =item * C<use_json>
1802              
1803             Defaults to true.
1804              
1805             A boolean value (C<1> or C<0>) to indicate whether to use JSON format.
1806              
1807             =back
1808              
1809             =head2 METHODS
1810              
1811             =head2 add_element
1812              
1813             my $elem = $po->add_element( $element_object,
1814             after => 'Some other text',
1815             );
1816             my $elem = $po->add_element(
1817             msgid => 'Hello world!",
1818             msgstr => 'Salut tout le monde !',
1819             comment => 'No comment',
1820             before => 'Some other text', # Add this new element before this msgid/include directive
1821             );
1822              
1823             This takes either of the following parameters, and adds the new element, if it does not already exist, to the list of elements:
1824              
1825             =over 4
1826              
1827             =item 1. L<Text::PO::Element> object + C<%options>
1828              
1829             A L<Text::PO::Element> object, possibly followed by an hash or hash reference of options.
1830              
1831             =item 2. C<%options>
1832              
1833             An hash or hash ref of options that will be passed to L<Text::PO::Element> to create a new object.
1834              
1835             =back
1836              
1837             It returns the newly created element if it did not already exist, or the existing one found. Thus if you try to add an element data that already exists, this will prevent it and return the existing element object found.
1838              
1839             If an error occurred, it will set an L<error object|Module::Generic::Exception> and return C<undef> in scalar context, or an empty list in list context.
1840              
1841             Supported options are:
1842              
1843             =over 4
1844              
1845             =item * all the ones used in L<Text::PO::Element>
1846              
1847             =item * C<before> / C<after>
1848              
1849             A C<msgid> or C<include> directive value to add this element before or after.
1850              
1851             =back
1852              
1853             =head2 add_include
1854              
1855             my $elem = $po->add_include( $element_object,
1856             after => 'Some other text',
1857             );
1858             my $elem = $po->add_include(
1859             file => 'include/me.po",
1860             comment => 'No comment',
1861             before => 'Some other text', # Add this new element before this msgid/include directive
1862             );
1863              
1864             This takes either of the following parameters, and adds the new include directive, if it does not already exist, to the list of elements:
1865              
1866             =over 4
1867              
1868             =item 1. L<Text::PO::Element> object + C<%options>
1869              
1870             A L<Text::PO::Element> object, possibly followed by an hash or hash reference of options.
1871              
1872             =item 2. C<%options>
1873              
1874             An hash or hash ref of options that will be passed to L<Text::PO::Element> to create a new object.
1875              
1876             =back
1877              
1878             Note that the C<file> parameter must be set in the element passed, or provided among the options used to create a new element.
1879              
1880             It returns the newly created element if it did not already exist, or the existing one found. Thus if you try to add an include directive that already exists, this will prevent it and return the existing element object found.
1881              
1882             If an error occurred, it will set an L<error object|Module::Generic::Exception> and return C<undef> in scalar context, or an empty list in list context.
1883              
1884             Supported options are:
1885              
1886             =over 4
1887              
1888             =item * all the ones used in L<Text::PO::Element>
1889              
1890             =item * C<before> / C<after>
1891              
1892             A C<msgid> or C<include> directive value to add this element before or after.
1893              
1894             =back
1895              
1896             =head2 added
1897              
1898             Returns an array object (L<Module::Generic::Array>) of L<Text::PO::Element> objects added during synchronisation.
1899              
1900             =head2 as_json
1901              
1902             This takes an optional hash reference of option parameters and return a json formatted string.
1903              
1904             All options take a boolean value. Possible options are:
1905              
1906             =over 4
1907              
1908             =item * C<indent>
1909              
1910             If true, L<JSON> will indent the data.
1911              
1912             Default to false.
1913              
1914             =item * C<pretty>
1915              
1916             If true, this will return a human-readable json data.
1917              
1918             =item * C<sort>
1919              
1920             If true, this will instruct L<JSON> to sort the keys. This makes it slower to generate.
1921              
1922             It defaults to false, which will use a pseudo random order set by perl.
1923              
1924             =item * C<utf8>
1925              
1926             If true, L<JSON> will utf8 encode the data.
1927              
1928             =back
1929              
1930             =head2 as_hash
1931              
1932             Return the data parsed as an hash reference.
1933              
1934             =head2 as_string
1935              
1936             Serializes the current PO object into a single string containing valid GNU C<.po> syntax. This is equivalent to calling L</dump> into an in-memory scalar, but more convenient for tests or further processing.
1937              
1938             my $string = $po->as_string;
1939              
1940             This always returns a plain Perl string (not a blessed scalar or IO object) to avoid issues with string overloading.
1941              
1942             =head2 charset
1943              
1944             Sets or gets the character encoding for the po data. This will affect the C<charset> parameter in C<Content-Type> meta information.
1945              
1946             =head2 content_encoding
1947              
1948             Sets or gets the meta field value for C<Content-Encoding>
1949              
1950             =head2 content_type
1951              
1952             Sets or gets the meta field value for C<Content-Type>
1953              
1954             =head2 current_lang
1955              
1956             Returns the current language environment variable set, trying C<LANGUAGE> and C<LANG>
1957              
1958             =head2 decode
1959              
1960             Given a string, this will decode it using the character set specified with L</encoding>
1961              
1962             =head2 domain
1963              
1964             Sets or gets the domain (or namespace) for this PO. Something like C<com.example.api>
1965              
1966             =head2 dump
1967              
1968             Given an optional filehandle, or STDOUT by default, it will print to that filehandle in a format suitable to the po file.
1969              
1970             Thus, one could create a perl script, read a po file, then redirect the output of the dump back to another po file like
1971              
1972             ./po_script.pl en_GB.po > new_en_GB.po
1973              
1974             It returns the L<Text::PO> object used.
1975              
1976             =head2 elements
1977              
1978             Returns the array reference of all the L<Text::PO::Element> objects
1979              
1980             =head2 encoding
1981              
1982             Sets or gets the character set encoding for the GNU PO file. Typically this should be C<utf-8>
1983              
1984             =head2 exists
1985              
1986             Given a L<Text::PO::Element> object, it will check if this object exists in its current stack. To achieve this, it will check if both the C<msgid> and the C<msgstr> exists and match. If you only want to check if the C<msgid> exists, use the C<msgid_only> option as explained below.
1987              
1988             It takes an optional hash or hash reference of options as follows:
1989              
1990             =over 4
1991              
1992             =item * C<msgid_only>
1993              
1994             Boolean. If true, this will check only if the C<msgid> already exists, and not the corresponding C<msgstr>
1995              
1996             =back
1997              
1998             It returns true of false accordingly.
1999              
2000             =head2 hash
2001              
2002             Returns the data of the po file as an hash reference with each key representing a string and its value the localised version.
2003              
2004             =head2 header
2005              
2006             Access the headers data for this po file. The data is an array reference.
2007              
2008             =head2 include
2009              
2010             $po->include(1); # enable include directives
2011             $po->include(0); # disable include directives
2012             my $bool = $po->include;
2013              
2014             Controls whether C<$include "file.po"> directives are recognised during parsing.
2015              
2016             Include support is enabled by default.
2017              
2018             Include directives may appear in comments, using one of the following forms:
2019              
2020             # $include "other.po"
2021             #. $include 'relative/path.po'
2022             # $include "shared/common.po"
2023              
2024             When include processing is enabled, any referenced file is parsed recursively. Only valid PO entries (C<msgid>/C<msgstr>/C<msgid_plural>/C<msgctxt> blocks and special comments) from included files are merged into the caller’s namespace; header blocks and meta sections of include files are ignored.
2025              
2026             This feature allows modular PO files, shared error message bundles, and structured localisation domains without a separate preprocessing step.
2027              
2028             =head2 language
2029              
2030             Sets or gets the meta field value for C<Language>
2031              
2032             =head2 language_team
2033              
2034             Sets or gets the meta field value for C<Language-Team>
2035              
2036             =head2 last_translator
2037              
2038             Sets or gets the meta field value for C<Last-Translator>
2039              
2040             =head2 max_recurse
2041              
2042             $po->max_recurse(20);
2043             my $limit = $po->max_recurse;
2044              
2045             Sets or gets the maximum recursion depth allowed when processing include directives.
2046              
2047             The default is 32.
2048              
2049             If the recursion limit is exceeded (for example because of accidental self-inclusion or a circular include chain), parsing will abort and L</error> will contain a descriptive message including the file path and line number where recursion overflow occurred.
2050              
2051             This protects users from infinite loops and malicious PO input.
2052              
2053             =head2 merge
2054              
2055             This takes the same parameters as L</sync> and will merge the current data with the target data and return the newly created L<Text::PO> object
2056              
2057             =head2 meta
2058              
2059             This sets or return the given meta information. The meta field name provided is case insensitive and you can replace dashes (C<->) with underscore (<_>)
2060              
2061             $po->meta( 'Project-Id-Version' => 'MyProject 1.0' );
2062             # or this will also work
2063             $po->meta( project_id_version => 'MyProject 1.0' );
2064              
2065             It can take a hash ref, a hash, or a single element. If a single element is provided, it return its corresponding value.
2066              
2067             This returns its internal hash of meta information.
2068              
2069             =head2 meta_keys
2070              
2071             This is an hash reference of meta information.
2072              
2073             =head2 mime_version
2074              
2075             Sets or gets the meta field value for C<MIME-Version>
2076              
2077             =head2 new_element
2078              
2079             Provided with an hash or hash reference of property-value pairs, and this will pass those information to L<Text::PO::Element> and return the new object.
2080              
2081             =head2 normalise_meta
2082              
2083             Given a meta field, this will return a normalised version of it, ie a field name with the right case and dash instead of underscore characters.
2084              
2085             =head2 parse
2086              
2087             $po->parse( $filepath );
2088             $po->parse( $filepath, include => 0 );
2089             $po->parse( $fh, max_recurse => 20 );
2090              
2091             Parses a GNU C<.po> file or a filehandle and loads its entries into the current object. Returns the current L<Text::PO> instance on success. Upon error, it sets an L<error object|Module::Generic::Exception> and returns C<undef> in scalar context, and an empty list in list context.
2092              
2093             =head3 Include processing
2094              
2095             If include processing is enabled (see L</include>), the parser recognises the following non-standard directives:
2096              
2097             # $include "path.po"
2098             #.$include "relative.po"
2099              
2100             Relative paths are resolved against the directory of the parent file.
2101              
2102             When an include directive is seen:
2103              
2104             =over 4
2105              
2106             =item 1.
2107              
2108             A new L<Text::PO> object is created for the included file.
2109              
2110             =item 2.
2111              
2112             The effective C<include> and C<max_recurse> settings are passed to the child parser.
2113              
2114             =item 3.
2115              
2116             Only PO elements (C<msgid>/C<msgstr>/C<msgctl>/C<msgid_plural> entries, and special comments) from the included file are merged into the parent’s C<elements> list. Header metadata from included files is ignored.
2117              
2118             =item 4.
2119              
2120             Circular references are detected. A descriptive error is attached to the directive line and parsing continues for the parent file.
2121              
2122             =item 5.
2123              
2124             If the included file has some header meta information containing the header C<Language> and if it does not match that of the parent, a warning is emitted if warnings are enabled.
2125              
2126             =back
2127              
2128             =head3 Options
2129              
2130             parse() accepts the following options:
2131              
2132             =over 4
2133              
2134             =item * C<include> (boolean)
2135              
2136             Override the parser’s include behaviour for this parse call.
2137              
2138             =item * C<max_recurse> (unsigned integer)
2139              
2140             Override the maximum include depth for this parse call.
2141              
2142             =back
2143              
2144             =head2 parse_date_to_object
2145              
2146             Provided with a date string and this returns a L<DateTime> object
2147              
2148             =head2 parse_header_value
2149              
2150             Takes a header value such as C<text/plain; charset="utf-8"> and this returns a C<Text::PO::HeaderValue> object
2151              
2152             =head2 parse2hash
2153              
2154             Whether the pod file is stored as standard GNU po data or as json data, this method will read its data and return an hash reference of it.
2155              
2156             =head2 parse2object
2157              
2158             Takes a file path, parse the po file and loads its data onto the current object. It returns the current object.
2159              
2160             =head2 plural
2161              
2162             Sets or gets the plurality definition for this domain and locale used in the current object.
2163              
2164             If set, this will expect 2 parameters: 1) an integer representing the possible plurality for the given locale and 2) the expression that will be evaluated to assess which plural form to use.
2165              
2166             It returns an array reference representing those 2 values.
2167              
2168             If you want to find out the proper plural form for a given C<locale>, you should refer to the L<Unicode CLDR|https://cldr.unicode.org/> data, which can be accessed and queries via the module L<Locale::Unicode::Data>:
2169              
2170             my $cldr = Locale::Unicode::Data->new;
2171             say $cldr->plural_forms( 'fr' ); # nplurals=2; plural=(n > 1);
2172              
2173             =head2 plural_forms
2174              
2175             Sets or gets the meta field value for C<Plural-Forms>
2176              
2177             =head2 po_revision_date
2178              
2179             Sets or gets the meta field value for C<PO-Revision-Date>
2180              
2181             =head2 pot_creation_date
2182              
2183             Sets or gets the meta field value for C<POT-Creation-Date>
2184              
2185             =head2 project_id_version
2186              
2187             Sets or gets the meta field value for C<Project-Id-Version>
2188              
2189             =head2 quote
2190              
2191             Given a string, it will escape carriage return, double quote and return it,
2192              
2193             =head2 remove_duplicates
2194              
2195             Takes a boolean value to enable or disable the removal of duplicates in the po file.
2196              
2197             =head2 remove_element
2198              
2199             Given a L<Text::PO::Element> and this will remove it from the object elements list.
2200              
2201             If the value provided is not an L<Text::PO::Element> object it will return an error.
2202              
2203             It returns a true value representing the number of elements removed or 0 if none could be found.
2204              
2205             =head2 removed
2206              
2207             Sets or gets this boolean value.
2208              
2209             =head2 report_bugs_to
2210              
2211             Sets or gets the meta field value for C<Report-Msgid-Bugs-To>
2212              
2213             =head2 quote
2214              
2215             Takes a string and escape the characters that needs to be and returns it.
2216              
2217             =head2 remove_duplicates
2218              
2219             Takes a boolean value and if true, this will remove duplicate msgid.
2220              
2221             =head2 removed
2222              
2223             Returns an array object (L<Module::Generic::Array>) of L<Text::PO::Element> removed during synchronisation.
2224              
2225             =head2 set_default_meta
2226              
2227             Applies a set of default meta information to the <.po> file, if missing.
2228              
2229             =head2 source
2230              
2231             Sets or gets an hash reference of parameters providing information about the source of the data.
2232              
2233             It could have an attribute C<handle> with a glob as value or an attribute C<file> with a filepath as value.
2234              
2235             =head2 sync
2236              
2237             $po->sync( '/some/where/com.example.api.po' );
2238             # or
2239             $po->sync({ file => '/some/where/com.example.api.po' });
2240             # or
2241             $po->sync({ handle => $file_handle });
2242             # or, if source of data has been set previously by parse()
2243             $po->parse( '/some/where/com.example.api.po' );
2244             # Do some change to the data, then:
2245             $po->sync;
2246              
2247             Given a file or a file handle, it will read the po file, and our current object will synchronise against it.
2248              
2249             It takes an hash or hash reference passed as argument, as optional parameters with the following properties:
2250              
2251             =over 4
2252              
2253             =item * C<file>
2254              
2255             File path
2256              
2257             =item * C<handle>
2258              
2259             Opened file handle
2260              
2261             =back
2262              
2263             This means that our object is the source and the file or filehandle representing the target po file is the recipient of the synchronisation.
2264              
2265             This method will return an error a file is provided, already exists, but is either a symbolic link or not a regular file (C<-f> test), or a file handle is provided, but not currently opened.
2266              
2267             If a file path is provided, and the file does not yet exist, it will attempt to create it or return an error if it cannot. In this case, it will use L</dump> to write all its data to file.
2268              
2269             If the target file was created, it will return the current object, otherwise it returns the newly created L<Text::PO> representing the data synchronised.
2270              
2271             =head2 sync_fh
2272              
2273             Takes a file handle as its unique argument and synchronise the object data with the file handle. This means, the file handle provided must be opened in both read and write mode.
2274              
2275             What it does is that, after creating a new L<Text::PO> object, it will first call L</parse> on the file handle to load its data, and then add all of the current object data to the newly created object, and finally dump all back to the file handle using L</dump>
2276              
2277             It will set two array of data: one for the elements that did not exist in the recipient data and thus were added and one for those elements in the target data that did not exist in the source object and thus were removed.
2278              
2279             If the option I<append> is specified, however, it will not remove those elements in the target that doe not exist in the source one. You can get the same result by calling the method L</merge> instead of L</sync>
2280              
2281             You can get the data of each of those 2 arrays by calling the methods L</added> and L</removed> respectively.
2282              
2283             It returns the newly created L<Text::PO> object containing the synchronised data.
2284              
2285             =head2 unquote
2286              
2287             Takes a string, unescape it and returns it.
2288              
2289             =head2 use_json
2290              
2291             Takes a boolean value and if true, this will save the data as json instead of regular po format.
2292              
2293             Saving data as json makes it quicker to load, but also enable the data to be used by JavaScript.
2294              
2295             =head1 PRIVATE METHODS
2296              
2297             =head2 _can_write_fh
2298              
2299             Given a filehandle, returns true if it can be written to it or false otherwise.
2300              
2301             =head2 _set_get_meta_date
2302              
2303             Takes a meta field name for a date-type field and sets its value, if one is provided, or returns a L<DateTime> object.
2304              
2305             If a value is provided, even a string, it will be converted to a L<DateTime> object and a L<DateTime::Format::Strptime> will be attached to it as a formatter so the stringification of the object produces a date compliant with PO format.
2306              
2307             =head2 _set_get_meta_value
2308              
2309             Takes a meta field name and sets or gets its value.
2310              
2311             =head1 THREAD-SAFETY
2312              
2313             This module is thread-safe. All state is stored on a per-object basis, and the underlying file operations and data structures do not share mutable global state.
2314              
2315             =head1 AUTHOR
2316              
2317             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
2318              
2319             =head1 SEE ALSO
2320              
2321             L<Text::PO::Element>, L<Text::PO::MO>, L<Text::PO::Gettext>
2322              
2323             L<https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html>,
2324              
2325             L<https://en.wikipedia.org/wiki/Gettext>
2326              
2327             L<GNU documentation on header format|https://www.gnu.org/software/gettext/manual/html_node/Header-Entry.html>
2328              
2329             =head1 COPYRIGHT & LICENSE
2330              
2331             Copyright (c) 2020-2025 DEGUEST Pte. Ltd.
2332              
2333             You can use, copy, modify and redistribute this package and associated files under the same terms as Perl itself.
2334              
2335             =cut