File Coverage

lib/Text/PO.pm
Criterion Covered Total %
statement 576 1233 46.7
branch 191 1002 19.0
condition 68 387 17.5
subroutine 75 97 77.3
pod 46 46 100.0
total 956 2765 34.5


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## PO Files Manipulation - ~/lib/Text/PO.pm
3             ## Version v0.6.2
4             ## Copyright(c) 2023 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2018/06/21
7             ## Modified 2023/10/15
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 4     4   743333 use strict;
  4         20  
  4         135  
17 4     4   30 use warnings;
  4         8  
  4         143  
18 4     4   21 use warnings::register;
  4         6  
  4         579  
19 4     4   527 use parent qw( Module::Generic );
  4         314  
  4         38  
20 4     4   122068 use vars qw( $VERSION @META $DEF_META );
  4         9  
  4         266  
21 4     4   1078 use open ':std' => ':utf8';
  4         2675  
  4         33  
22 4     4   1205 use Class::Struct;
  4         1986  
  4         51  
23 4     4   4333 use DateTime;
  4         2175176  
  4         271  
24 4     4   50 use DateTime::TimeZone;
  4         38  
  4         107  
25 4     4   45 use Encode ();
  4         10  
  4         85  
26 4     4   26 use Fcntl qw( :DEFAULT );
  4         8  
  4         1498  
27 4     4   2492 use JSON ();
  4         31478  
  4         130  
28 4     4   1722 use Nice::Try;
  4         231045  
  4         44  
29 4     4   11052211 use Scalar::Util;
  4         25  
  4         293  
30 4     4   3027 use Text::PO::Element;
  4         16  
  4         100  
31 4 50   4   1701 use constant HAS_LOCAL_TZ => ( eval( qq{DateTime::TimeZone->new( name => 'local' );} ) ? 1 : 0 );
  4         11  
  4         274  
32 4     4   16643 our $VERSION = 'v0.6.2';
33             };
34              
35 4     4   26 use strict;
  4         6  
  4         90  
36 4     4   18 use warnings;
  4         7  
  4         7917  
37              
38             struct 'Text::PO::Comment' =>
39             {
40             'text' => '@',
41             };
42             our @META = qw(
43             Project-Id-Version
44             Report-Msgid-Bugs-To
45             POT-Creation-Date
46             PO-Revision-Date
47             Last-Translator
48             Language-Team
49             Language
50             Plural-Forms
51             MIME-Version
52             Content-Type
53             Content-Transfer-Encoding
54             );
55             our $DEF_META =
56             {
57             'Project-Id-Version' => 'Project 0.1',
58             'Report-Msgid-Bugs-To' => 'bugs@example.com',
59             # 2011-07-02 20:53+0900
60             'POT-Creation-Date' => DateTime->from_epoch( 'epoch' => time(), 'time_zone' => ( HAS_LOCAL_TZ ? 'local' : 'UTC' ) )->strftime( '%Y-%m-%d %H:%M%z' ),
61             'PO-Revision-Date' => DateTime->from_epoch( 'epoch' => time(), 'time_zone' => ( HAS_LOCAL_TZ ? 'local' : 'UTC' ) )->strftime( '%Y-%m-%d %H:%M%z' ),
62             'Last-Translator' => 'Unknown <hello@example.com>',
63             'Language-Team' => 'Unknown <hello@example.com>',
64             'Language' => '',
65             'Plural-Forms' => 'nplurals=1; plural=0;',
66             'MIME-Version' => '1.0',
67             'Content-Type' => 'text/plain; charset=utf-8',
68             'Content-Transfer-Encoding' => '8bit',
69             };
70              
71             sub init
72             {
73 7     7 1 140549 my $self = shift( @_ );
74 7         370 $self->{domain} = '';
75 7         77 $self->{header} = [];
76             ## utf8
77 7         79 $self->{encoding} = '';
78 7         59 $self->{meta} = {};
79 7         55 $self->{meta_keys} = [];
80             ## Default to using po json file if it exists
81 7         54 $self->{use_json} = 1;
82 7         48 $self->{remove_duplicates} = 1;
83 7         44 $self->{_init_strict_use_sub} = 1;
84 7         95 $self->SUPER::init( @_ );
85 7         2848 $self->{elements} = [];
86 7         55 $self->{added} = [];
87 7         60 $self->{removed} = [];
88 7         69 $self->{source} = {};
89 7         33 return( $self );
90             }
91              
92             sub add_element
93             {
94 0     0 1 0 my $self = shift( @_ );
95 0         0 my $id;
96 0         0 my $opt = {};
97 0         0 my $e;
98 0 0 0     0 if( $self->_is_a( $_[0] => 'Text::PO::Element' ) )
    0          
    0          
99             {
100 0         0 $e = shift( @_ );
101 0         0 $id = $e->msgid;
102             }
103             elsif( scalar( @_ ) == 1 && ref( $_[0] ) eq 'HASH' )
104             {
105 0         0 $opt = shift( @_ );
106 0   0     0 $id = $opt->{msgid} || return( $self->error( "No msgid was provided" ) );
107 0         0 $e = Text::PO::Element->new( %$opt );
108             }
109             elsif( !( @_ % 2 ) )
110             {
111 0         0 $opt = { @_ };
112 0   0     0 $id = $opt->{msgid} || return( $self->error( "No msgid was provided" ) );
113 0         0 $e = Text::PO::Element->new( %$opt );
114             }
115             else
116             {
117 0         0 $id = shift( @_ );
118 0 0       0 $opt = { @_ } if( !( @_ % 2 ) );
119 0         0 $opt->{msgid} = $id;
120 0         0 $e = Text::PO::Element->new( %$opt );
121             }
122 0 0       0 return( $self->error( "No msgid was provided." ) ) if( !length( $id ) );
123 0         0 my $elem = $self->elements;
124 0         0 foreach my $e2 ( @$elem )
125             {
126 0         0 my $msgid = $e2->msgid;
127 0 0       0 my $thisId = ref( $msgid ) ? join( '', @$msgid ) : $msgid;
128 0 0       0 if( $thisId eq $id )
129             {
130             # return( $self->error( "There already is an id '$id' in the po file" ) );
131 0         0 return( $e2 );
132             }
133             }
134 0         0 $e->po( $self );
135 0         0 push( @{$self->{elements}}, $e );
  0         0  
136 0         0 return( $e );
137             }
138              
139 0     0 1 0 sub added { return( shift->_set_get_array_as_object( 'added', @_ ) ); }
140              
141 1     1 1 1891 sub as_hash { return( shift->hash( @_ ) ); }
142              
143             sub as_json
144             {
145 1     1 1 3198 my $self = shift( @_ );
146 1         14 my $opts = $self->_get_args_as_hash( @_ );
147 1         10 my $metaKeys = $self->{meta_keys};
148 1         3 my $hash = {};
149 1         4 $hash->{domain} = $self->domain;
150 1         743 $hash->{meta} = {};
151 1         9 $hash->{meta_keys} = [];
152 1         20 $hash->{elements} = [];
153 1         13 foreach my $k ( @$metaKeys )
154             {
155 11         27 my $key = $self->normalise_meta( $k );
156 11         32 my $val = $self->meta( $k );
157 11         282 $hash->{meta}->{ $key } = $val;
158 11         15 push( @{$hash->{meta_keys}}, $key );
  11         40  
159             }
160 1         15 my $elem = $self->elements;
161 1         806 foreach my $e ( @$elem )
162             {
163 8         20 my $msgid = $e->msgid;
164 8         141 my $msgstr = $e->msgstr;
165 8 50 33     22 next if( $e->is_meta || !CORE::length( $e->msgid ) );
166 8 100       177 my $k = ref( $msgid ) ? join( '', @$msgid ) : $msgid;
167             # my $v = ref( $msgstr ) ? join( '', @$msgstr ) : $msgstr;
168 8         12 my $v;
169 8 100       18 if( $e->plural )
170             {
171 1         740 my $res = [];
172 1         7 for( my $i = 0; $i < scalar( @$msgstr ); $i++ )
173             {
174 2 50       6 push( @$res, ref( $msgstr->[$i] ) ? join( '', @{$msgstr->[$i]} ) : $msgstr->[$i] );
  2         8  
175             }
176 1         8 $v = $res;
177             }
178             else
179             {
180 7 100       4727 $v = ref( $msgstr ) ? join( '', @$msgstr ) : $msgstr;
181             }
182            
183 8         65 my $ref =
184             {
185             msgid => $k,
186             msgstr => $v,
187             };
188 8 100 66     39 $ref->{msgid_plural} = $e->msgid_plural if( $e->plural && $e->msgid_plural );
189 8 50       4811 if( !scalar( @{$ref->{comment} = $e->comment} ) )
  8         26  
190             {
191 8         159 delete( $ref->{comment} );
192             }
193 8 50       21 if( !length( $ref->{context} = $e->context ) )
194             {
195 8         6261 delete( $ref->{context} );
196             }
197 8 50       11 if( !scalar( @{$ref->{flags} = $e->flags} ) )
  8         22  
198             {
199 8         158 delete( $ref->{flags} );
200             }
201 8 100       22 if( !length( $ref->{reference} = $e->reference ) )
202             {
203 5         9 delete( $ref->{reference} );
204             }
205 8         11 push( @{$hash->{elements}}, $ref );
  8         21  
206             }
207 1         104 my $j = JSON->new->relaxed->allow_blessed->convert_blessed;
208             # canonical = sorting hash keys
209 1         5 foreach my $t ( qw( pretty utf8 indent canonical ) )
210             {
211 4 50       8 $j->$t( $opts->{ $t } ) if( exists( $opts->{ $t } ) );
212             }
213 1 50       10 $j->canonical( $opts->{sort} ) if( exists( $opts->{sort} ) );
214 1 50 33     9 try
  1         2  
  1         46  
  1         9  
  0         0  
  1         3  
  1         9  
  1         3  
215 1     1   3 {
216 1         420 my $json = $j->encode( $hash );
217 1         19 return( $json );
218             }
219 1 0 0     17 catch( $e )
  0 0 33     0  
  0 0       0  
  1 0       5  
  1 0       2  
  1 0       3  
  1 0       7  
  1 0       11  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 0       0  
  0 50       0  
  0         0  
  0         0  
  0         0  
  1         7  
  0         0  
  1         3  
  0         0  
  0         0  
  1         12  
  1         16  
  1         9  
  1         7  
  0         0  
  0         0  
  0         0  
  0         0  
220 0     0   0 {
221 0         0 return( $self->error( "Unable to json encode the hash data created: $e" ) );
222 4 0 0 4   51 }
  4 0 0     7  
  4 0 33     6387  
  0 0 33     0  
  0 0 33     0  
  0 0 33     0  
  0 0 33     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  1 0       5  
  0 0       0  
  1 50       20  
  1 50       31  
  1 50       17  
  0 0       0  
  0 50       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  1         38  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
223             }
224              
225             sub charset
226             {
227 2     2 1 995 my $self = shift( @_ );
228 2         10 my $type = $self->content_type();
229 2         52 my $def = $self->parse_header_value( $type );
230 2 50       11 if( @_ )
231             {
232 0         0 my $v = shift( @_ );
233 0         0 $def->params->{charset} = $v;
234 0         0 $self->meta( content_type => $def->as_string );
235             }
236 2         46 return( $def->params->{charset} );
237             }
238              
239 2     2 1 980 sub content_encoding { return( shift->_set_get_meta_value( 'Content-Transfer-Encoding' ) ); }
240              
241 4     4 1 857 sub content_type { return( shift->_set_get_meta_value( 'Content-Type' ) ); }
242              
243             # <https://superuser.com/questions/392439/lang-and-language-environment-variable-in-debian-based-systems>
244             sub current_lang
245             {
246 2     2 1 836 my $self = shift( @_ );
247 2 50 33     83 return( '' ) if( !CORE::exists( $ENV{LANGUAGE} ) && !CORE::exists( $ENV{LANG} ) );
248 0 0 0     0 return( ( $ENV{LANGUAGE} || $ENV{LANG} ) ? [split( /:/, ( $ENV{LANGUAGE} || $ENV{LANG} ) )]->[0] : '' );
      0        
249             }
250              
251             sub decode
252             {
253 0     0 1 0 my $self = shift( @_ );
254 0         0 my $str = shift( @_ );
255 0 0       0 return( '' ) if( !length( $str ) );
256 0         0 my $enc = $self->encoding;
257 0 0       0 return( $str ) if( !$enc );
258 0 0 0     0 try
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
259 0     0   0 {
260 0 0       0 return( Encode::decode_utf8( $str, Encode::FB_CROAK ) ) if( $enc eq 'utf8' );
261 0         0 return( Encode::decode( $enc, $str, Encode::FB_CROAK ) );
262             }
263 0 0 0     0 catch( $e )
  0 0 0     0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
264 0     0   0 {
265 0         0 return( $self->error( "An error occurred while trying to decode a string using encoding '$enc': $e" ) );
266 4 0 0 4   31 }
  4 0 0     7  
  4 0 0     15079  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
267             }
268              
269 11     11 1 46741 sub domain { return( shift->_set_get_scalar( 'domain', @_ ) ); }
270              
271             sub dump
272             {
273 1     1 1 134288 my $self = shift( @_ );
274 1         17 my $fh = IO::File->new;
275 1 50       60 if( @_ )
276             {
277 1         10 $fh = shift( @_ );
278 1 50       20 return( $self->error( "Filehandle provided '$fh' (", ref( $fh ), ") does not look like a filehandle" ) ) if( !Scalar::Util::openhandle( $fh ) );
279             # $fh->fdopen( fileno( $fh ), 'w' );
280             }
281             else
282             {
283 0         0 $fh->fdopen( fileno( STDOUT ), 'w' );
284             }
285 1   50     22 my $enc = $self->encoding || 'utf8';
286 1 50       820 $enc = 'utf8' if( lc( $enc ) eq 'utf-8' );
287 1 50       12 $fh->binmode( ":${enc}" ) || return( $self->error( "Unable to set binmode on character encoding '$enc': $!" ) );
288 1         138 $fh->autoflush(1);
289 1         135 my $elem = $self->{elements};
290 1 50       13 if( my $header = $self->header )
291             {
292 1 50       870 $fh->print( join( "\n", @$header ) ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
293             }
294 1         246 my $domain = $self->domain;
295 1 50       757 if( length( $domain ) )
296             {
297 1 50       15 $fh->print( "\n#\n# domain \"${domain}\"" ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
298             }
299 1 50       146 $fh->print( "\n\n" ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
300             ## my $metaKeys = $self->meta_keys;
301 1         180 my $metaKeys = [@META];
302 1 50       17 if( scalar( @$metaKeys ) )
303             {
304 1 50       10 $fh->printf( "msgid \"\"\n" ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
305 1 50       188 $fh->printf( "msgstr \"\"\n" ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
306 1         141 foreach my $k ( @$metaKeys )
307             {
308 11         1489 my $k2 = lc( $k );
309 11         28 $k2 =~ tr/-/_/;
310 11 50 33     66 if( !exists( $self->{meta}->{ $k2 } ) &&
311             length( $DEF_META->{ $k } ) )
312             {
313 0         0 $self->{meta}->{ $k2 } = $DEF_META->{ $k };
314             }
315 11 50       343 $fh->printf( "\"%s: %s\\n\"\n", $self->normalise_meta( $k ), $self->meta( $k ) ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
316             }
317 1 50       160 $fh->print( "\n" ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
318             }
319 1         134 foreach my $e ( @$elem )
320             {
321 8 50 33     911 next if( $e->is_meta || !CORE::length( $e->msgid ) );
322 8 50       225 if( $e->po ne $self )
323             {
324 0 0       0 warnings::warn( "This element '", $e->msgid, "' does not belong to us. Its po object is different than our current object.\n" ) if( warnings::enabled() );
325             }
326 8 50       238 $fh->print( $e->dump, "\n" ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
327 8 50       1108 $fh->print( "\n" ) || return( $self->error( "Unable to print po data to file handle: $!" ) );
328             }
329 1         143 return( $self );
330             }
331              
332 10     10 1 76580 sub elements { return( shift->_set_get_array_as_object( 'elements', @_ ) ); }
333              
334 79     79 1 19773 sub encoding { return( shift->_set_get_scalar( 'encoding', @_ ) ); }
335              
336             sub exists
337             {
338 1     1 1 623 my $self = shift( @_ );
339 1   50     11 my $elem = shift( @_ ) || return( $self->error( "No element to check existence was provided." ) );
340 1 50       28 return( $self->error( "The element provided is not an Text::PO::Element object" ) ) if( !$self->_is_a( $elem => 'Text::PO::Element' ) );
341 1         106 my $opts = $self->_get_args_as_hash( @_ );
342 1   50     34 $opts->{msgid_only} //= 0;
343 1         8 my $elems = $self->{elements};
344             # No need to go further if the object provided does not even have a msgid
345 1 50       5 return(0) if( !length( $elem->msgid ) );
346 1         33 foreach my $e ( @$elems )
347             {
348 1 50 33     20 if( ( $opts->{msgid_only} && $e->msgid eq $elem->msgid ) ||
      33        
      33        
349             ( $e->msgid eq $elem->msgid && $e->msgstr eq $elem->msgstr ) )
350             {
351 1 50       7 if( length( $elem->context ) )
352             {
353 0 0       0 if( $elem->context eq $e->context )
354             {
355 0         0 return(1);
356             }
357             }
358             else
359             {
360 1         742 return(1);
361             }
362             }
363             }
364 0         0 return(0);
365             }
366              
367             sub hash
368             {
369 1     1 1 13 my $self = shift( @_ );
370 1         9 my $elem = $self->elements;
371 1         1080 my $hash = {};
372 1         7 foreach my $e ( @$elem )
373             {
374 8         31 my $msgid = $e->msgid;
375 8         213 my $msgstr = $e->msgstr;
376 8 100       30 my $k = ref( $msgid ) ? join( '', @$msgid ) : $msgid;
377 8 100       28 my $v = ref( $msgstr ) ? join( '', @$msgstr ) : $msgstr;
378 8         34 $hash->{ $k } = $v;
379             }
380 1         17 return( $self->new_hash( $hash ) );
381             }
382              
383 3     3 1 46 sub header { return( shift->_set_get_array_as_object( 'header', @_ ) ); }
384              
385 2     2 1 44 sub language { return( shift->_set_get_meta_value( 'Language' ) ); }
386              
387 2     2 1 919 sub language_team { return( shift->_set_get_meta_value( 'Language-Team' ) ); }
388              
389 2     2 1 850 sub last_translator { return( shift->_set_get_meta_value( 'Last-Translator' ) ); }
390              
391             sub merge
392             {
393 0     0 1 0 my $self = shift( @_ );
394 0         0 my $opts = $self->_get_args_as_hash( @_ );
395 0         0 $opts->{merge} = 1;
396 0         0 return( $self->sync( $opts ) );
397             }
398              
399             sub meta
400             {
401 59     59 1 1010 my $self = shift( @_ );
402 59 100       531 if( @_ )
403             {
404 54 100       235 if( $self->_is_hash( $_[0] ) )
    50          
    0          
405             {
406 4         72 $self->{meta} = shift( @_ );
407             }
408             elsif( scalar( @_ ) == 1 )
409             {
410 50         569 my $k = shift( @_ );
411 50         102 $k =~ tr/-/_/;
412 50         466 return( $self->{meta}->{ lc( $k ) } );
413             }
414             elsif( !( @_ % 2 ) )
415             {
416 0         0 my $this = { @_ };
417 0         0 foreach my $k ( keys( %$this ) )
418             {
419 0         0 my $k2 = $k;
420 0         0 $k2 =~ tr/-/_/;
421 0         0 $self->{meta}->{ lc( $k2 ) } = $this->{ $k };
422             }
423             }
424             else
425             {
426 0         0 return( $self->error( "Unknown data provided: '", join( "', '", @_ ), "'." ) );
427             }
428            
429 4         22 foreach my $k ( keys( %{$self->{meta}} ) )
  4         47  
430             {
431 34 100       126 if( CORE::index( $k, '-' ) != -1 )
432             {
433 31         46 my $k2 = $k;
434 31         53 $k2 =~ tr/-/_/;
435 31         115 $self->{meta}->{ $k2 } = CORE::delete( $self->{meta}->{ $k } );
436             }
437             }
438             }
439 9         91 return( $self->_set_get_hash_as_mix_object( 'meta' ) );
440             }
441              
442             sub meta_keys
443             {
444 3     3 1 39734 my $self = shift( @_ );
445 3 100       20 if( @_ )
446             {
447 1         3 my $ref = shift( @_ );
448 1 50       20 return( $self->error( "Value provided is not an array reference." ) ) if( !$self->_is_array( $ref ) );
449 1         21 my $copy = [@$ref];
450 1         3 for( @$copy )
451             {
452 1         7 tr/-/_/;
453 1         6 $_ = lc( $_ );
454             }
455 1         6 $self->{meta_keys} = $copy;
456             }
457 3         23 my $data = $self->{meta_keys};
458 3 50       20 $data = [sort( keys( %{$self->{meta}} ) )] if( !scalar( @$data ) );
  0         0  
459 3         8 my $new = [];
460 3         17 for( @$data )
461             {
462 23         67 push( @$new, $self->normalise_meta( $_ ) );
463             }
464 3         64 return( $self->new_array( $new ) );
465             }
466              
467 2     2 1 37229 sub mime_version { return( shift->_set_get_meta_value( 'MIME-Version' ) ); }
468              
469             sub new_element
470             {
471 9     9 1 15 my $self = shift( @_ );
472 9         25 my $opts = $self->_get_args_as_hash( @_ );
473 9         1092 $opts->{po} = $self;
474 9         50 my $e = Text::PO::Element->new( $opts );
475 9 50 33     64 $e->encoding( $self->encoding ) if( !$opts->{encoding} && $self->encoding );
476 9         5805 $e->debug( $self->debug );
477 9         319 return( $e );
478             }
479              
480             sub normalise_meta
481             {
482 46     46 1 878 my $self = shift( @_ );
483 46   50     138 my $str = shift( @_ ) || return( '' );
484 46         79 $str =~ tr/_/-/;
485 46         1283 my @res = grep( /^$str$/i, @META );
486 46 100       146 if( scalar( @res ) )
487             {
488 45         175 return( $res[0] );
489             }
490 1         11 return( '' );
491             }
492              
493             sub parse
494             {
495 3     3 1 490887 my $self = shift( @_ );
496 3   50     25 my $this = shift( @_ ) || return( $self->error( "No file or glob was provided to parse po file." ) );
497 3         63 my $io;
498 3         21 my $fh_was_open = 0;
499 3 50       104 if( Scalar::Util::reftype( $this ) eq 'GLOB' )
500             {
501 0         0 $io = $this;
502 0 0       0 return( $self->error( "Filehandle provided '$io' is not opened" ) ) if( !Scalar::Util::openhandle( $io ) );
503 0         0 $fh_was_open++;
504 0         0 $self->source({ handle => $this });
505             }
506             else
507             {
508 3   50     31 $io = IO::File->new( "<$this" ) || return( $self->error( "Unable to open po file \"$this\" in read mode: $!" ) );
509             ## By default
510 3         894 $self->source({ file => $this });
511             }
512 3         12449 $io->binmode( ':utf8' );
513 3         86 my $elem = [];
514 3         27 $self->{elements} = $elem;
515 3         135 my $header = '';
516 3         15 my $ignoring_leading_blanks = 1;
517 3         17 my $n = 0;
518             # Ignore / remove possible leading blank lines
519 3         186 while( defined( $_ = $io->getline ) )
520             {
521 15         594 $n++;
522 15 100 33     79 if( /^\S+/ )
    50          
523             {
524 13         24 $ignoring_leading_blanks = 0;
525             }
526             elsif( $ignoring_leading_blanks && /^[[:blank:]\h]*$/ )
527             {
528 0         0 next;
529             }
530             #( 1 .. /^[^\#]+$/ ) or last;
531 15 100       67 /^\#+/ || last;
532 12 100       45 if( /^\#+[[:blank:]\h]*domain[[:blank:]]+\"([^\"]+)\"/ )
533             {
534 2         91 $self->domain( $1 );
535 2         1718 $self->message_colour( 3, "Setting domain to <green>$1</>" );
536             }
537             else
538             {
539 10         40 $header .= $_;
540             }
541             }
542             # Remove trailing blank lines from header
543 3         83 $header =~ s/(^[[:blank:]\h]*\#[[:blank:]\h]*\n$)+\Z//gms;
544             ## Make sure to position ourself after the initial blank line if any, since blank lines are used as separators
545             ## Actually, no we don't care. Blocks are: maybe some comments, msgid then msgstr. That's how we delimit them
546             ## $_ = $io->getline while( /^[[:blank:]]*$/ && defined( $_ ) );
547 3 100       84 $self->header( [ split( /\n/, $header ) ] ) if( length( $header ) );
548 3         1663 my $e = Text::PO::Element->new( po => $self );
549 3         57 $e->debug( $self->debug );
550             ## What was the last seen element?
551             ## This is used for multi line buffer, so we know where to add it
552 3         156 my $lastSeen = '';
553 3         5 my $foundFirstLine = 0;
554             ## To keep track of the msgid found so we can skip duplicates
555 3         11 my $seen = {};
556 3         35 while( defined( $_ = $io->getline ) )
557             {
558 128         4652 $n++;
559 128         191 chomp( $_ );
560 128 100 66     270 if( !$foundFirstLine && /^\S/ )
561             {
562 3         8 $foundFirstLine++;
563             }
564 128 100       1445 if( /^[[:blank:]]*$/ )
    50          
    100          
    50          
    50          
    100          
    100          
    100          
    100          
    100          
    50          
565             {
566 21 50       57 if( $foundFirstLine )
567             {
568             ## Case where msgid and msgstr are separated by a blank line
569 21 50 66     75 if( scalar( @$elem ) > 1 &&
      33        
      33        
      0        
570             !length( $e->msgid ) &&
571             length( $e->msgstr ) &&
572             length( $elem->[-1]->msgid ) &&
573             !length( $elem->[-1]->msgstr ) )
574             {
575 0         0 $elem->[-1]->merge( $e );
576             }
577             else
578             {
579 21 50       361 if( ++$seen->{ $e->id } > 1 )
580             {
581 0         0 next;
582             }
583 21         47 push( @$elem, $e );
584             }
585 21         88 $e = Text::PO::Element->new( po => $self );
586 21         118 $e->{_po_line} = $n;
587 21 100       73 $e->encoding( $self->encoding ) if( $self->encoding );
588 21         13931 $e->debug( $self->debug );
589             }
590             ## special treatment for first item that contains the meta information
591 21 100       875 if( scalar( @$elem ) == 1 )
592             {
593 3         12 my $this = $elem->[0];
594 3         28 my $def = $this->msgstr;
595 3         71 $def = [split( /\n/, join( '', @$def ) )];
596            
597 3         13 my $meta = {};
598 3         21 foreach my $s ( @$def )
599             {
600 33         49 chomp( $s );
601 33 50       182 if( $s =~ /^([^\x00-\x1f\x80-\xff :=]+):[[:blank:]]*(.*?)$/ )
602             {
603 33         130 my( $k, $v ) = ( lc( $1 ), $2 );
604 33         170 $meta->{ $k } = $v;
605 33         44 push( @{$self->{meta_keys}}, $k );
  33         78  
606 33 100       83 if( $k eq 'content-type' )
607             {
608 3 50       67 if( $v =~ /\bcharset=\s*([-\w]+)/i )
609             {
610             # my $enc = lc( $1 );
611 3         13 my $enc = $1;
612             ## See PerlIO::encoding man page
613 3 50       15 $enc = 'utf8' if( lc( $enc ) eq 'utf-8' );
614 3         23 $self->encoding( $enc );
615 3 50 33     2552 try
  3         13  
  3         10  
  3         20  
  0         0  
  3         22  
  3         59  
  3         20  
616 3     3   8 {
617 3 50       37 $io->binmode( $enc eq 'utf8' ? ":$enc" : ":encoding($enc)" );
618             }
619 3 50 100     57 catch( $e )
  3 0 33     80  
  1 0       9  
  3 0       10  
  3 0       8  
  3 0       5  
  3 0       6  
  3 0       11  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0 100       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 0       0  
  0 50       0  
  0         0  
  0         0  
  0         0  
  3         23  
  0         0  
  1         3  
  2         12  
  2         13  
  3         11  
  3         13  
  3         6  
  3         13  
  0         0  
  0         0  
  0         0  
  0         0  
620 0     0   0 {
621 0         0 return( $self->error( "Unable to set binmode to charset \"$enc\": $e" ) );
622 4 0 0 4   33 }
  4 0 0     20  
  4 0 66     10483  
  0 0 33     0  
  0 0 66     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 33     0  
  0 0 66     0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  3 0       17  
  0 0       0  
  3 0       137  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         21  
  0         0  
  0         0  
  0         0  
  0         0  
  3         29  
623             }
624             }
625             }
626             }
627 3 50       24 if( scalar( keys( %$meta ) ) )
628             {
629 3         19 $self->meta( $meta );
630 3         7009 $this->is_meta( 1 );
631             }
632             }
633             }
634             ## #. TRANSLATORS: A test phrase with all letters of the English alphabet.
635             ## #. Replace it with a sample text in your language, such that it is
636             ## #. representative of language's writing system.
637             elsif( /^\#\.[[:blank:]]*(.*?)$/ )
638             {
639 0         0 my $c = $1;
640 0         0 $e->add_auto_comment( $c );
641             }
642             ## #: finddialog.cpp:38
643             ## #: colorscheme.cpp:79 skycomponents/equator.cpp:31
644             elsif( /^\#\:[[:blank:]]+(.*?)$/ )
645             {
646 9         34 my $c = $1;
647 9         65 $e->reference( $c );
648             }
649             ## #, c-format
650             elsif( /^\#\,[[:blank:]]+(.*?)$/ )
651             {
652 0         0 my $c = $1;
653 0 0       0 $e->flags( [ split( /[[:blank:]]*,[[:blank:]]*/, $c ) ] ) if( $c );
654             }
655             elsif( /^\#+[[:blank:]]+(.*?)$/ )
656             {
657 0         0 my $c = $1;
658 0 0 0     0 if( !$self->meta->length && $c =~ /^domain[[:blank:]\h]+\"(.*?)\"/ )
659             {
660 0         0 $self->domain( $1 );
661             }
662             else
663             {
664 0         0 $e->add_comment( $c);
665             }
666             }
667             elsif( /^msgid[[:blank:]]+"(.*?)"$/ )
668             {
669 22 100       160 $e->msgid( $self->unquote( $1 ) ) if( length( $1 ) );
670 22         495 $lastSeen = 'msgid';
671             }
672             ## #: mainwindow.cpp:127
673             ## #, kde-format
674             ## msgid "Time: %1 second"
675             ## msgid_plural "Time: %1 seconds"
676             ## msgstr[0] "Tiempo: %1 segundo"
677             ## msgstr[1] "Tiempo: %1 segundos"
678             elsif( /^msgid_plural[[:blank:]]+"(.*?)"[[:blank:]]*$/ )
679             {
680 3 50       38 $e->msgid_plural( $self->unquote( $1 ) ) if( length( $1 ) );
681 3         75 $e->plural(1);
682 3         4728 $lastSeen = 'msgid_plural';
683             }
684             ## disambiguating context:
685             ## #: tools/observinglist.cpp:700
686             ## msgctxt "First letter in 'Scope'"
687             ## msgid "S"
688             ## msgstr ""
689             ##
690             ## #: skycomponents/horizoncomponent.cpp:429
691             ## msgctxt "South"
692             ## msgid "S"
693             ## msgstr ""
694             elsif( /^msgctxt[[:blank:]]+"(.*?)"[[:blank:]]*$/ )
695             {
696 2 50       52 $e->context( $self->unquote( $1 ) ) if( length( $1 ) );
697 2         2557 $lastSeen = 'msgctxt';
698             }
699             elsif( /^msgstr[[:blank:]]+"(.*?)"[[:blank:]]*$/ )
700             {
701 20 100       136 $e->msgstr( $self->unquote( $1 ) ) if( length( $1 ) );
702 20         87 $lastSeen = 'msgstr';
703             }
704             elsif( /^msgstr\[(\d+)\][[:blank:]]+"(.*?)"[[:blank:]]*$/ )
705             {
706 8 50       46 if( length( $2 ) )
707             {
708 8         35 $e->msgstr( $1, $self->unquote( $2 ) );
709 8         55 $e->plural(1);
710             }
711 8         12508 $lastSeen = 'msgstr';
712             }
713             elsif( /^[[:blank:]]*"(.*?)"[[:blank:]]*$/ )
714             {
715 43         98 my $sub = "add_${lastSeen}";
716 43 50       158 if( $e->can( $sub ) )
717             {
718 43 50       224 $e->$sub( $self->unquote( $1 ) ) if( length( $1 ) );
719             }
720             else
721             {
722 0         0 warn( "Unable to find method \"${sub}\" in class \"", ref( $e ), "\" for line parsed \"$_\"\n" );
723             }
724             }
725             else
726             {
727 0 0       0 warnings::warn( "I do not understand the line \"$_\" at line $n\n" ) if( warnings::enabled() );
728             }
729             }
730 3 50       58 $io->close unless( $fh_was_open );
731 3 50       85 if( scalar( @$elem ) )
732             {
733 3 100 66     58 push( @$elem, $e ) if( $elem->[-1] ne $e && CORE::length( $e->msgid ) && ++$seen->{ $e->msgid } < 2 );
      66        
734 3 50       104 shift( @$elem ) if( $elem->[0]->is_meta );
735             }
736 3         2088 return( $self );
737             }
738              
739             sub parse_date_to_object
740             {
741 4     4 1 12 my $self = shift( @_ );
742 4         23 my $str = shift( @_ );
743 4   50     114 my $d = $self->_parse_timestamp( $str ) ||
744             return( $self->error( "Date time string provided is unsupported: \"${str}\"." ) );
745 4         4092702 my $strp = $d->formatter;
746 4 50       30 unless( $strp )
747             {
748 0         0 $strp = DateTime::Format::Strptime->new(
749             pattern => '%Y-%m-%d %H:%M%z',
750             locale => 'en_GB',
751             time_zone => $d->time_zone,
752             );
753 0         0 $d->set_formatter( $strp );
754             }
755 4         87 return( $d );
756             }
757              
758             sub parse_header_value
759             {
760 2     2 1 7 my $self = shift( @_ );
761 2         10 my $s = shift( @_ );
762 2 50 33     20 return( $self->error( 'Argument string is required' ) ) if( !defined( $s ) || !length( $s ) );
763 2 50       17 my $sep = @_ ? shift( @_ ) : ';';
764 2         10 my @parts = ();
765 2         6 my $i = 0;
766 2         110 foreach( split( /(\\.)|$sep/, $s ) )
767             {
768 6 100       18 defined( $_ ) ? do{ $parts[$i] .= $_ } : do{ $i++ };
  4         10  
  2         4  
769             }
770 2         8 my $header_val = shift( @parts );
771 2         59 my $obj = Text::PO::HeaderValue->new( $header_val );
772            
773 2         17 my $param = {};
774 2         12 foreach my $frag ( @parts )
775             {
776 2         26 $frag =~ s/^[[:blank:]]+|[[:blank:]]+$//g;
777 2         37 my( $attribute, $value ) = split( /[[:blank:]]*\=[[:blank:]]*/, $frag, 2 );
778 2         15 $value =~ s/^\"|\"$//g;
779             ## Check character string and length. Should not be more than 255 characters
780             ## http://tools.ietf.org/html/rfc1341
781             ## http://www.iana.org/assignments/media-types/media-types.xhtml
782             ## Won't complain if this does not meet our requirement, but will discard it silently
783 2 50 33     53 if( $attribute =~ /^[a-zA-Z][a-zA-Z0-9\_\-]+$/ && CORE::length( $attribute ) <= 255 )
784             {
785 2 50 33     37 if( $value =~ /^[a-zA-Z][a-zA-Z0-9\_\-]+$/ && CORE::length( $value ) <= 255 )
786             {
787 2         25 $obj->param( lc( $attribute ) => $value );
788             }
789             }
790             }
791 2         11 return( $obj );
792             }
793              
794             sub parse2hash
795             {
796 0     0 1 0 my $self = shift( @_ );
797 0   0     0 my $this = shift( @_ ) || return( $self->error( "No file or glob was provided to parse po file." ) );
798 0         0 my $buff = '';
799 0 0 0     0 if( $self->{use_json} && ( -e( "${this}.json" ) || $this =~ /\.json$/ ) )
      0        
800             {
801 0 0       0 my $file = -e( "${this}.json" ) ? "${this}.json" : $this;
802 0   0     0 my $io = IO::File->new( "$file" ) || return( $self->error( "Unable to open json po file \"${file}\" in read mode: $!" ) );
803 0         0 $io->binmode( ':utf8' );
804 0         0 $io->read( $buff, -s( $file ) );
805 0         0 $io->close;
806 0         0 my $j = JSON->new->relaxed;
807 0         0 my $ref = {};
808 0 0 0     0 try
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
809 0     0   0 {
810 0         0 $ref = $j->decode( $buff );
811             }
812 0 0 0     0 catch( $e )
  0 0 0     0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
813 0     0   0 {
814 0         0 return( $self->error( "An error occurred while json decoding data from \"${file}\": $e" ) );
815 4 0 0 4   36 }
  4 0 0     8  
  4 0 0     5991  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
816 0         0 my $hash = {};
817 0         0 foreach my $elem ( @{$ref->{elements}} )
  0         0  
818             {
819 0         0 $hash->{ $elem->{msgid} } = $elem->{msgstr};
820             }
821 0         0 return( $self->new_hash( $hash ) );
822             }
823             else
824             {
825 0 0       0 $self->parse( $this ) || return( $self->pass_error );
826 0         0 return( $self->hash );
827             }
828             }
829              
830             sub parse2object
831             {
832 2     2 1 10 my $self = shift( @_ );
833 2   50     16 my $this = shift( @_ ) || return( $self->error( "No file or glob was provided to parse po file." ) );
834 2         39 my $buff = '';
835 2 50 33     22 if( $self->{use_json} && ( -e( "${this}.json" ) || $this =~ /\.json$/ ) )
      33        
836             {
837 2 50       253 my $file = -e( "${this}.json" ) ? "${this}.json" : $this;
838 2   50     150 my $io = IO::File->new( $file ) || return( $self->error( "Unable to open json po file \"${file}\" in read mode: $!" ) );
839 2         748 $io->binmode( ':utf8' );
840 2         71 $io->read( $buff, -s( $file ) );
841 2         313 $io->close;
842 2         176 my $j = JSON->new->relaxed;
843 2         12 my $ref = {};
844 2 50 33     37 try
  2         7  
  2         15  
  2         53  
  0         0  
  2         7  
  2         59  
  2         28  
845 2     2   9 {
846 2         217 $ref = $j->decode( $buff );
847             }
848 2 0 50     38 catch( $e )
  2 0 33     14  
  2 0       21  
  2 0       12  
  2 0       5  
  2 0       11  
  2 0       14  
  2 0       24  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 50       0  
  0 0       0  
  0 50       0  
  0         0  
  0         0  
  0         0  
  2         26  
  0         0  
  2         14  
  0         0  
  0         0  
  2         26  
  2         17  
  2         6  
  2         16  
  0         0  
  0         0  
  0         0  
  0         0  
849 0     0   0 {
850 0         0 return( $self->error( "An error occurred while json decoding data from \"${file}\": $e" ) );
851 4 0 0 4   63 }
  4 0 0     12  
  4 0 33     9501  
  0 0 33     0  
  0 0 33     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 0     0  
  0 0 33     0  
  0 0 33     0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  2 0       20  
  0 0       0  
  2 0       231  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 50       0  
  0 50       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  2         15  
  0         0  
  0         0  
  0         0  
  0         0  
  2         21  
852            
853 2 50 33     26 $self->domain( $ref->{domain} ) if( length( $ref->{domain} ) && !length( $self->domain ) );
854 2         1660 my $meta_keys = [];
855 2 50       43 if( $ref->{meta_keys} )
    0          
856             {
857 2         8 $meta_keys = $ref->{meta_keys};
858             }
859             elsif( $ref->{meta} )
860             {
861 0         0 $meta_keys = [sort( keys( %{$ref->{meta}} ) )];
  0         0  
862             }
863            
864 2 50       14 if( $ref->{meta} )
865             {
866 2         9 $self->{meta} = {};
867 2         9 foreach my $k ( keys( %{$ref->{meta}} ) )
  2         36  
868             {
869 22         71 my $k2 = lc( $k );
870 22         44 $k2 =~ tr/-/_/;
871 22         150 $self->{meta}->{ $k2 } = $ref->{meta}->{ $k };
872             }
873             }
874 2         15 $self->{meta_keys} = $meta_keys;
875            
876 2 50       21 if( scalar( @$meta_keys ) )
877             {
878 2         55 my $e = Text::PO::Element->new( 'po' => $self );
879 2         35 $e->debug( $self->debug );
880 2         126 $e->msgid( '' );
881             $e->msgstr(
882 2         148 [map( sprintf( '%s: %s', $_, $ref->{meta}->{ $_ } ), @$meta_keys )]
883             );
884 2         23 $e->is_meta(1);
885 2         1782 push( @{$self->{elements}}, $e );
  2         9  
886             }
887            
888 2         12 foreach my $def ( @{$ref->{elements}} )
  2         15  
889             {
890 16         70 my $e = Text::PO::Element->new( 'po' => $self );
891 16         130 $e->debug( $self->debug );
892 16         619 $e->msgid( $def->{msgid} );
893 16 100       334 if( $def->{msgid_plural} )
894             {
895 2         42 $e->msgid_plural( $def->{msgid_plural} );
896             }
897 16 100       114 if( ref( $def->{msgstr} ) eq 'ARRAY' )
898             {
899 2         9 for( my $i = 0; $i < scalar( @{$def->{msgstr}} ); $i++ )
  5         18  
900             {
901 3         12 $e->msgstr( $i => $def->{msgstr}->[$i] );
902             }
903             }
904             else
905             {
906 14         431 $e->msgstr( $def->{msgstr} );
907             }
908 16 50       54 $e->comment( $def->{comment} ) if( $def->{comment} );
909 16 50       39 $e->context( $def->{context} ) if( $def->{context} );
910 16 50       39 $e->flags( $def->{flags} ) if( $def->{flags} );
911 16 100       63 $e->reference( $def->{reference} ) if( $def->{reference} );
912 16 50       60 $e->encoding( $self->encoding ) if( $self->encoding );
913 16         10917 push( @{$self->{elements}}, $e );
  16         56  
914             }
915 2         80 return( $self );
916             }
917             else
918             {
919 0         0 return( $self->parse( $this ) );
920             }
921             }
922              
923             sub plural
924             {
925 2     2 1 12 my $self = shift( @_ );
926 2 50       17 if( @_ )
927             {
928 0         0 my( $nplurals, $expr ) = @_;
929 0         0 $self->{plural} = [ $nplurals, $expr ];
930 0         0 return( [ @{$self->{plural}} ] );
  0         0  
931             }
932             else
933             {
934 2 0 50     10 return( [@{$self->{plural}}] ) if( $self->{plural} && scalar( @{$self->{plural}} ) );
  0         0  
  0         0  
935 2         18 my $meta = $self->meta;
936 2         2416 my $pluralDef = $self->meta( 'Plural-Forms' );
937 2 50       58 if( $pluralDef )
938             {
939 2 50       45 if( $pluralDef =~ /^[[:blank:]\h]*nplurals[[:blank:]\h]*=[[:blank:]\h]*(\d+)[[:blank:]\h]*\;[[:blank:]\h]*plural[[:blank:]\h]*=[[:blank:]\h]*(.*?)\;?$/i )
940             {
941 2         22 $self->{plural} = [ $1, $2 ];
942 2         16 return( $self->{plural} );
943             }
944             else
945             {
946 0         0 return( $self->error( "Malformed plural definition found in po data in meta field \"Plural-Forms\": " . $pluralDef ) );
947             }
948             }
949 0         0 return( [] );
950             }
951             }
952              
953 3     3 1 2441 sub plural_forms { return( shift->_set_get_meta_value( 'Plural-Forms', @_ ) ); }
954              
955 3     3 1 2322 sub po_revision_date { return( shift->_set_get_meta_date( 'PO-Revision-Date', @_ ) ); }
956              
957 1     1 1 7 sub pot_creation_date { return( shift->_set_get_meta_date( 'POT-Creation-Date', @_ ) ); }
958              
959 2     2 1 13 sub project_id_version { return( shift->_set_get_meta_value( 'Project-Id-Version', @_ ) ); }
960              
961 2     2 1 679 sub report_bugs_to { return( shift->_set_get_meta_value( 'Report-Msgid-Bugs-To', @_ ) ); }
962              
963             sub quote
964             {
965 20     20 1 486 my $self = shift( @_ );
966 20         28 my $str = shift( @_ );
967 20 50       47 return( '' ) if( !length( $str ) );
968             ## \t is a tab
969 20         57 $str =~ s/(?<!\\)\\(?!t)/\\\\/g;
970 20         36 $str =~ s/(?<!\\)"/\\"/g;
971 20         36 $str =~ s/(?<!\\)\n/\\n/g;
972 20         103 return( sprintf( '%s', $str ) );
973             }
974              
975 0     0 1 0 sub remove_duplicates { return( shift->_set_get_boolean( 'remove_duplicates', @_ ) ); }
976              
977             sub remove_element
978             {
979 0     0 1 0 my $self = shift( @_ );
980 0         0 my $elem = shift( @_ );
981 0         0 my $rv = $self->exists( $elem );
982 0 0       0 return if( !defined( $rv ) );
983 0 0       0 return(0) if( !$rv );
984 0         0 my $elems = $self->elements;
985 0         0 my $found = 0;
986 0         0 for( my $i = 0; $i < scalar( @$elems ); $i++ )
987             {
988 0 0       0 if( $elems->[$i] eq $elem )
989             {
990 0         0 splice( @$elems, $i, 1 );
991 0         0 $i--;
992 0         0 $found++;
993             }
994             }
995 0         0 return( $found );
996             }
997              
998 0     0 1 0 sub removed { return( shift->_set_get_array_as_object( 'removed', @_ ) ); }
999              
1000 3     3 1 169 sub source { return( shift->_set_get_hash_as_object( 'source', @_ ) ); }
1001              
1002             sub sync
1003             {
1004 0     0 1 0 my $self = shift( @_ );
1005             # a filehandle, or a filename?
1006             # my $this = shift( @_ ) || return( $self->error( "No file or filehandle provided." ) );
1007 0         0 my $this;
1008 0 0 0     0 $this = shift( @_ ) if( scalar( @_ ) && ( ( @_ % 2 ) || ( !( @_ % 2 ) && ref( $_[1] ) eq 'HASH' ) ) );
      0        
1009 0         0 my $opts = $self->_get_args_as_hash( @_ );
1010 0 0 0     0 $this = ( $opts->{handle} || $opts->{file} ) if( !CORE::length( $this ) );
1011 0 0       0 if( !$this )
1012             {
1013 0         0 my $fh;
1014 0 0       0 if( $fh = $self->source->handle )
    0          
1015             {
1016 0 0       0 $this = $fh if( $self->_can_write_fh( $fh ) );
1017             }
1018             elsif( my $file = $self->source->file )
1019             {
1020 0 0 0     0 $this = $file if( -e( $file ) && -w( $file ) );
1021 0   0     0 $fh = IO::File->new( ">$file" ) || return( $self->error( "Unable to open file \"$file\" in write mode: $!" ) );
1022             }
1023 0 0       0 return( $self->error( "No writable file handle or file set to sync our data against." ) ) if( !$this );
1024 0         0 $fh->binmode( ':utf8' );
1025 0 0       0 $self->dump( $fh ) || return( $self->pass_error );
1026 0         0 $fh->close;
1027 0         0 return( $self );
1028             }
1029            
1030 0 0       0 if( Scalar::Util::reftype( $this ) eq 'GLOB' )
    0          
    0          
1031             {
1032 0 0       0 return( $self->error( "Filehandle provided is not opened" ) ) if( !Scalar::Util::openhandle( $this ) );
1033 0 0       0 return( $self->error( "Filehandle provided is not writable" ) ) if( !$self->_can_write_fh( $this ) );
1034 0         0 return( $self->sync_fh( $this, $opts ) );
1035             }
1036             elsif( -l( $this ) )
1037             {
1038 0         0 return( $self->error( "File provided is actually a symbolic link. Do not want to write to a symbolic link." ) );
1039             }
1040             elsif( -e( $this ) )
1041             {
1042 0 0       0 if( !-f( $this ) )
1043             {
1044 0         0 return( $self->error( "File '$this' is not a file." ) );
1045             }
1046 0   0     0 my $fh = IO::File->new( "+<$this" ) || return( $self->error( "Unable to open file '$this' in read/write mode: $!" ) );
1047 0         0 my $po = $self->sync_fh( $fh, $opts );
1048 0         0 $fh->close;
1049 0         0 return( $po );
1050             }
1051             # Does not exist yet
1052             else
1053             {
1054 0   0     0 my $fh = IO::File->new( ">$this" ) || return( $self->error( "Unable to write to file '$this': $!" ) );
1055 0 0       0 $self->dump( $fh ) || return( $self->pass_error );
1056 0         0 $fh->close;
1057             }
1058 0         0 return( $self );
1059             }
1060              
1061             sub sync_fh
1062             {
1063 0     0 1 0 my $self = shift( @_ );
1064 0         0 my $fh = shift( @_ );
1065 0 0       0 return( $self->error( "Filehandle provided $fh is not a valid file handle" ) ) if( !Scalar::Util::openhandle( $fh ) );
1066 0         0 my $opts = $self->_get_args_as_hash( @_ );
1067             # Parse file
1068 0         0 my $po = $self->new;
1069 0         0 $po->debug( $self->debug );
1070 0         0 $po->parse( $fh );
1071             # Remove the ones that do not exist
1072 0         0 my $elems = $po->elements;
1073 0         0 my @removed = ();
1074 0         0 for( my $i = 0; $i < scalar( @$elems ); $i++ )
1075             {
1076 0         0 my $e = $elems->[$i];
1077 0 0       0 if( !$self->exists( $e, { msgid_only => 1 } ) )
1078             {
1079 0         0 my $removedObj = splice( @$elems, $i, 1 );
1080 0 0       0 push( @removed, $removedObj ) if( $removedObj );
1081             }
1082             }
1083             # Now check each one of ours against this parsed file and add our items if missing
1084 0         0 $elems = $self->elements;
1085 0         0 my @added = ();
1086 0         0 foreach my $e ( @$elems )
1087             {
1088 0 0       0 if( !$po->exists( $e, { msgid_only => 1 } ) )
1089             {
1090 0         0 $po->add_element( $e );
1091 0         0 push( @added, $e );
1092             }
1093             }
1094             # Now, rewind and rewrite the file
1095 0 0       0 $fh->seek(0,0) || return( $self->error( "Unable to seek file handle!: $!" ) );
1096             # $fh->print( $po->dump );
1097 0 0       0 $po->dump( $fh ) || return( $self->pass_error );
1098 0         0 $fh->truncate( $fh->tell );
1099 0         0 $po->added( \@added );
1100 0         0 $po->removed( \@removed );
1101 0         0 return( $po );
1102             }
1103              
1104             sub unquote
1105             {
1106 89     89 1 141 my $self = shift( @_ );
1107 89         153 my $str = shift( @_ );
1108 89 50       215 return( '' ) if( !length( $str ) );
1109 89         216 $str =~ s/^"(.*)"/$1/;
1110 89         133 $str =~ s/\\"/"/g;
1111             ## newline
1112 89         263 $str =~ s/(?<!(\\))\\n/\n/g;
1113             ## inline newline
1114 89         163 $str =~ s/(?<!(\\))\\{2}n/\\n/g;
1115             ## \ followed by newline
1116 89         130 $str =~ s/(?<!(\\))\\{3}n/\\\n/g;
1117             ## \ followed by inline newline
1118 89         132 $str =~ s/\\{4}n/\\\\n/g;
1119             ## all slashes not related to a newline
1120 89         127 $str =~ s/\\\\(?!n)/\\/g;
1121 89         390 return( $str );
1122             }
1123              
1124 2     2 1 1706 sub use_json { return( shift->_set_get_boolean( 'use_json', @_ ) ); }
1125              
1126             ## https://stackoverflow.com/questions/3807231/how-can-i-test-if-i-can-write-to-a-filehandle
1127             ## -> https://stackoverflow.com/a/3807381/4814971
1128             sub _can_write_fh
1129             {
1130 0     0   0 my $self = shift( @_ );
1131 0         0 my $fh = shift( @_ );
1132 0         0 my $flags = fcntl( $fh, F_GETFL, 0 );
1133 0 0       0 if( ( $flags & O_ACCMODE ) & ( O_WRONLY|O_RDWR ) )
1134             {
1135 0         0 return(1);
1136             }
1137 0         0 return(0);
1138             }
1139              
1140             sub _set_get_meta_date
1141             {
1142 4     4   24 my $self = shift( @_ );
1143 4   50     30 my $field = shift( @_ ) || return( $self->error( "No field was provided to get its DateTime object equivalent." ) );
1144 4 50       52 if( @_ )
1145             {
1146 0         0 my $v = shift( @_ );
1147 0 0 0     0 if( ref( $v ) && $self->_is_a( $v => 'DateTime' ) )
1148             {
1149 0         0 my $strp = DateTime::Format::Strptime->new(
1150             pattern => '%F %H:%M%z',
1151             locale => 'en_GB',
1152             time_zone => ( HAS_LOCAL_TZ ? 'local' : 'UTC' ),
1153             );
1154 0         0 $v->set_formatter( $strp );
1155             }
1156 0         0 $self->meta( $field => $v );
1157 0         0 return( $v );
1158             }
1159             else
1160             {
1161 4         41 my $meta = $self->meta( $field );
1162 4 50 33     177 if( !defined( $meta ) || !length( $meta ) )
1163             {
1164 0         0 return;
1165             }
1166 4         43 return( $self->parse_date_to_object( $meta ) );
1167             }
1168             }
1169              
1170             sub _set_get_meta_value
1171             {
1172 21     21   79 my $self = shift( @_ );
1173 21   50     182 my $field = shift( @_ ) || return( $self->error( "No field was provided to get its DateTime object equivalent." ) );
1174 21 50       82 if( @_ )
1175             {
1176 0         0 my $v = shift( @_ );
1177 0         0 $self->meta( $field => $v );
1178             }
1179 21         91 return( $self->meta( $field ) );
1180             }
1181              
1182             # NOTE: Text::PO::HeaderValue class
1183             {
1184             package
1185             Text::PO::HeaderValue;
1186             BEGIN
1187             {
1188 4     4   34 use strict;
  4         10  
  4         105  
1189 4     4   18 use warnings;
  4         8  
  4         143  
1190 4     4   18 use parent qw( Module::Generic );
  4         11  
  4         27  
1191 4     4   293 use vars qw( $VERSION $QUOTE_REGEXP $TYPE_REGEXP $TOKEN_REGEXP $TEXT_REGEXP );
  4         6  
  4         368  
1192 4     4   858 our $VERSION = 'v0.1.0';
1193             use overload (
1194 4         41 '""' => 'as_string',
1195             fallback => 1,
1196 4     4   29 );
  4         7  
1197 4         28 our $QUOTE_REGEXP = qr/([\\"])/;
1198             #
1199             # RegExp to match type in RFC 7231 sec 3.1.1.1
1200             #
1201             # media-type = type "/" subtype
1202             # type = token
1203             # subtype = token
1204             #
1205 4         213 our $TYPE_REGEXP = qr/^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
1206 4         110 our $TOKEN_REGEXP = qr/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
1207 4         106 our $TEXT_REGEXP = qr/^[\u000b\u0020-\u007e\u0080-\u00ff]+$/;
1208             };
1209            
1210 4     4   32 use strict;
  4         7  
  4         103  
1211 4     4   27 use warnings;
  4         10  
  4         2199  
1212            
1213             sub init
1214             {
1215 2     2   205 my $self = shift( @_ );
1216 2         6 my $value = shift( @_ );
1217 2 50       11 return( $self->error( "No value provided." ) ) if( !length( $value ) );
1218 2         131 $self->{original} = '';
1219 2         18 $self->{value} = $value;
1220 2         24 $self->SUPER::init( @_ );
1221 2         205 $self->{params} = {};
1222 2         10 return( $self );
1223             }
1224            
1225             sub as_string
1226             {
1227 0     0   0 my $self = shift( @_ );
1228 0 0 0     0 if( !defined( $self->{original} ) || !length( $self->{original} ) )
1229             {
1230 0         0 my $string = '';
1231 0 0 0     0 if( defined( $self->{value} ) && length( $self->{value} ) )
1232             {
1233 0 0       0 if( $self->{value} !~ /^$TYPE_REGEXP$/ )
1234             {
1235 0         0 return( $self->error( "Invalid value \"$self->{value}\"" ) );
1236             }
1237 0         0 $string = $self->{value};
1238             }
1239              
1240             # Append parameters
1241 0 0 0     0 if( $self->{params} && ref( $self->{params} ) eq 'HASH' )
1242             {
1243 0         0 my $params = [ sort( keys( %{$self->{params}} ) ) ];
  0         0  
1244 0         0 for( my $i = 0; $i < scalar( @$params ); $i++ )
1245             {
1246 0 0       0 if( $params->[$i] !~ /^$TOKEN_REGEXP$/ )
1247             {
1248 0         0 return( $self->error( "Invalid parameter name: \"" . $params->[$i] . "\"" ) );
1249             }
1250 0 0       0 if( length( $string ) > 0 )
1251             {
1252 0         0 $string .= '; ';
1253             }
1254 0         0 $string .= $params->[$i] . '=' . $self->qstring( $self->{params}->{ $params->[$i] } );
1255             }
1256             }
1257 0         0 $self->{original} = $string;
1258             }
1259 0         0 return( $self->{original} );
1260             }
1261            
1262 0     0   0 sub original { return( shift->_set_get_scalar_as_object( 'original', @_ ) ); }
1263            
1264             sub param
1265             {
1266 2     2   10 my $self = shift( @_ );
1267 2   50     20 my $name = shift( @_ ) || return( $self->error( "No parameter name was provided." ) );
1268 2 50       9 if( @_ )
1269             {
1270 2         8 my $v = shift( @_ );
1271 2         15 $self->{params}->{ $name } = $v;
1272             }
1273 2         13 return( $self->{params}->{ $name } );
1274             }
1275            
1276             sub qstring
1277             {
1278 0     0     my $self = shift( @_ );
1279 0           my $str = shift( @_ );
1280              
1281             # no need to quote tokens
1282 0 0         if( $str =~ /^$TOKEN_REGEXP$/ )
1283             {
1284 0           return( $str );
1285             }
1286              
1287 0 0 0       if( length( $str ) > 0 && $str !~ /^$TEXT_REGEXP$/ )
1288             {
1289 0           return( $self->error( 'Invalid parameter value' ) );
1290             }
1291            
1292 0           $str =~ s/$QUOTE_REGEXP/\\$1/g;
1293 0           return( '"' . $str . '"' );
1294             }
1295            
1296 0     0     sub value { return( shift->_set_get_scalar_as_object( 'value', @_ ) ); }
1297             }
1298              
1299             1;
1300             # NOTE: POD
1301             __END__
1302              
1303             =head1 NAME
1304              
1305             Text::PO - Read and write PO files
1306              
1307             =head1 SYNOPSIS
1308              
1309             use Text::PO;
1310             my $po = Text::PO->new;
1311             $po->debug( 2 );
1312             $po->parse( $poFile ) || die( $po->error, "\n" );
1313             my $hash = $po->as_hash;
1314             my $json = $po->as_json;
1315             # Add data:
1316             my $e = $po->add_element(
1317             msgid => 'Hello!',
1318             msgstr => 'Salut !',
1319             );
1320             $po->remove_element( $e );
1321             $po->elements->foreach(sub
1322             {
1323             my $e = shift( @_ ); # $_ is also available
1324             if( $e->msgid eq $other->msgid )
1325             {
1326             # do something
1327             }
1328             });
1329            
1330             # Write in a PO format to STDOUT
1331             $po->dump;
1332             # or to a file handle
1333             $po->dump( $io );
1334             # Synchronise data
1335             $po->sync( '/some/where/com.example.api.po' );
1336             $po->sync( $file_handle );
1337             # or merge
1338             $po->merge( '/some/where/com.example.api.po' );
1339             $po->merge( $file_handle );
1340              
1341             =head1 VERSION
1342              
1343             v0.6.2
1344              
1345             =head1 DESCRIPTION
1346              
1347             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.
1348              
1349             L<Text::PO::MO> reads and writes C<.mo> (machine object) binary files.
1350              
1351             Thus, with those modules, you do not need to install C<msgfmt>, C<msginit> of GNU. It is better if you have them though.
1352              
1353             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.
1354              
1355             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.
1356              
1357             =head1 CONSTRUCTOR
1358              
1359             =head2 new
1360              
1361             Create a new Text::PO object acting as an accessor.
1362              
1363             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.
1364              
1365             Returns the object.
1366              
1367             =head2 METHODS
1368              
1369             =head2 add_element
1370              
1371             Given either a L<Text::PO::Element> object, or an hash ref with keys like C<msgid> and C<msgstr>, or given a C<msgid> followed by an optional hash ref, L</add_element> will add this to the stack of elements.
1372              
1373             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.
1374              
1375             =head2 added
1376              
1377             Returns an array object (L<Module::Generic::Array>) of L<Text::PO::Element> objects added during synchronisation.
1378              
1379             =head2 as_json
1380              
1381             This takes an optional hash reference of option parameters and return a json formatted string.
1382              
1383             All options take a boolean value. Possible options are:
1384              
1385             =over 4
1386              
1387             =item * C<indent>
1388              
1389             If true, L<JSON> will indent the data.
1390              
1391             Default to false.
1392              
1393             =item * C<pretty>
1394              
1395             If true, this will return a human-readable json data.
1396              
1397             =item * C<sort>
1398              
1399             If true, this will instruct L<JSON> to sort the keys. This makes it slower to generate.
1400              
1401             It defaults to false, which will use a pseudo random order set by perl.
1402              
1403             =item * C<utf8>
1404              
1405             If true, L<JSON> will utf8 encode the data.
1406              
1407             =back
1408              
1409             =head2 as_hash
1410              
1411             Return the data parsed as an hash reference.
1412              
1413             =head2 as_json
1414              
1415             Return the PO data parsed as json data.
1416              
1417             =head2 charset
1418              
1419             Sets or gets the character encoding for the po data. This will affect the C<charset> parameter in C<Content-Type> meta information.
1420              
1421             =head2 content_encoding
1422              
1423             Sets or gets the meta field value for C<Content-Encoding>
1424              
1425             =head2 content_type
1426              
1427             Sets or gets the meta field value for C<Content-Type>
1428              
1429             =head2 current_lang
1430              
1431             Returns the current language environment variable set, trying C<LANGUAGE> and C<LANG>
1432              
1433             =head2 decode
1434              
1435             Given a string, this will decode it using the character set specified with L</encoding>
1436              
1437             =head2 domain
1438              
1439             Sets or gets the domain (or namespace) for this PO. Something like C<com.example.api>
1440              
1441             =head2 dump
1442              
1443             Given an optional filehandle, or STDOUT by default, it will print to that filehandle in a format suitable to the po file.
1444              
1445             Thus, one could create a perl script, read a po file, then redirect the output of the dump back to another po file like
1446              
1447             ./po_script.pl en_GB.po > new_en_GB.po
1448              
1449             It returns the L<Text::PO> object used.
1450              
1451             =head2 elements
1452              
1453             Returns the array reference of all the L<Text::PO::Element> objects
1454              
1455             =head2 encoding
1456              
1457             Sets or gets the character set encoding for the GNU PO file. Typically this should be C<utf-8>
1458              
1459             =head2 exists
1460              
1461             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.
1462              
1463             It takes an optional hash or hash reference of options as follows:
1464              
1465             =over 4
1466              
1467             =item * C<msgid_only>
1468              
1469             Boolean. If true, this will check only if the C<msgid> already exists, and not the corresponding C<msgstr>
1470              
1471             =back
1472              
1473             It returns true of false accordingly.
1474              
1475             =head2 hash
1476              
1477             Returns the data of the po file as an hash reference with each key representing a string and its value the localised version.
1478              
1479             =head2 header
1480              
1481             Access the headers data for this po file. The data is an array reference.
1482              
1483             =head2 language
1484              
1485             Sets or gets the meta field value for C<Language>
1486              
1487             =head2 language_team
1488              
1489             Sets or gets the meta field value for C<Language-Team>
1490              
1491             =head2 last_translator
1492              
1493             Sets or gets the meta field value for C<Last-Translator>
1494              
1495             =head2 merge
1496              
1497             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
1498              
1499             =head2 meta
1500              
1501             This sets or return the given meta information. The meta field name provided is case insensitive and you can replace dashes (C<->) with underscore (<_>)
1502              
1503             $po->meta( 'Project-Id-Version' => 'MyProject 1.0' );
1504             # or this will also work
1505             $po->meta( project_id_version => 'MyProject 1.0' );
1506              
1507             It can take a hash ref, a hash, or a single element. If a single element is provided, it return its corresponding value.
1508              
1509             This returns its internal hash of meta information.
1510              
1511             =head2 meta_keys
1512              
1513             This is an hash reference of meta information.
1514              
1515             =head2 mime_version
1516              
1517             Sets or gets the meta field value for C<MIME-Version>
1518              
1519             =head2 new_element
1520              
1521             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.
1522              
1523             =head2 normalise_meta
1524              
1525             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.
1526              
1527             =head2 parse
1528              
1529             Given a filepath to a po file or a file handle, this will parse the po file and return a new L<Text::PO> object.
1530              
1531             For each new entry that L</parse> find, it creates a L<Text::PO::Element> object.
1532              
1533             The list of all elements found can then be accessed using L</elements>
1534              
1535             It returns the current L<Text::PO> object
1536              
1537             =head2 parse_date_to_object
1538              
1539             Provided with a date string and this returns a L<DateTime> object
1540              
1541             =head2 parse_header_value
1542              
1543             Takes a header value such as C<text/plain; charset="utf-8"> and this returns a C<Text::PO::HeaderValue> object
1544              
1545             =head2 parse2hash
1546              
1547             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.
1548              
1549             =head2 parse2object
1550              
1551             Takes a file path, parse the po file and loads its data onto the current object. It returns the current object.
1552              
1553             =head2 plural
1554              
1555             Sets or gets the plurality definition for this domain and locale used in the current object.
1556              
1557             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.
1558              
1559             It returns an array reference representing those 2 values.
1560              
1561             =head2 plural_forms
1562              
1563             Sets or gets the meta field value for C<Plural-Forms>
1564              
1565             =head2 po_revision_date
1566              
1567             Sets or gets the meta field value for C<PO-Revision-Date>
1568              
1569             =head2 pot_creation_date
1570              
1571             Sets or gets the meta field value for C<POT-Creation-Date>
1572              
1573             =head2 project_id_version
1574              
1575             Sets or gets the meta field value for C<Project-Id-Version>
1576              
1577             =head2 quote
1578              
1579             Given a string, it will escape carriage return, double quote and return it,
1580              
1581             =head2 remove_duplicates
1582              
1583             Takes a boolean value to enable or disable the removal of duplicates in the po file.
1584              
1585             =head2 remove_element
1586              
1587             Given a L<Text::PO::Element> and this will remove it from the object elements list.
1588              
1589             If the value provided is not an L<Text::PO::Element> object it will return an error.
1590              
1591             It returns a true value representing the number of elements removed or 0 if none could be found.
1592              
1593             =head2 removed
1594              
1595             Sets or gets this boolean value.
1596              
1597             =head2 report_bugs_to
1598              
1599             Sets or gets the meta field value for C<Report-Msgid-Bugs-To>
1600              
1601             =head2 quote
1602              
1603             Takes a string and escape the characters that needs to be and returns it.
1604              
1605             =head2 remove_duplicates
1606              
1607             Takes a boolean value and if true, this will remove duplicate msgid.
1608              
1609             =head2 removed
1610              
1611             Returns an array object (L<Module::Generic::Array>) of L<Text::PO::Element> removed during synchronisation.
1612              
1613             =head2 source
1614              
1615             Sets or gets an hash reference of parameters providing information about the source of the data.
1616              
1617             It could have an attribute C<handle> with a glob as value or an attribute C<file> with a filepath as value.
1618              
1619             =head2 sync
1620              
1621             $po->sync( '/some/where/com.example.api.po' );
1622             # or
1623             $po->sync({ file => '/some/where/com.example.api.po' });
1624             # or
1625             $po->sync({ handle => $file_handle });
1626             # or, if source of data has been set previously by parse()
1627             $po->parse( '/some/where/com.example.api.po' );
1628             # Do some change to the data, then:
1629             $po->sync;
1630              
1631             Given a file or a file handle, it will read the po file, and our current object will synchronise against it.
1632              
1633             It takes an hash or hash reference passed as argument, as optional parameters with the following properties:
1634              
1635             =over 4
1636              
1637             =item I<file>
1638              
1639             File path
1640              
1641             =item I<handle>
1642              
1643             Opened file handle
1644              
1645             =back
1646              
1647             This means that our object is the source and the file or filehandle representing the target po file is the recipient of the synchronisation.
1648              
1649             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.
1650              
1651             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.
1652              
1653             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.
1654              
1655             =head2 sync_fh
1656              
1657             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.
1658              
1659             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>
1660              
1661             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.
1662              
1663             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>
1664              
1665             You can get the data of each of those 2 arrays by calling the methods L</added> and L</removed> respectively.
1666              
1667             It returns the newly created L<Text::PO> object containing the synchronised data.
1668              
1669             =head2 unquote
1670              
1671             Takes a string, unescape it and returns it.
1672              
1673             =head2 use_json
1674              
1675             Takes a boolean value and if true, this will save the data as json instead of regular po format.
1676              
1677             Saving data as json makes it quicker to load, but also enable the data to be used by JavaScript.
1678              
1679             =head1 PRIVATE METHODS
1680              
1681             =head2 _can_write_fh
1682              
1683             Given a filehandle, returns true if it can be written to it or false otherwise.
1684              
1685             =head2 _set_get_meta_date
1686              
1687             Takes a meta field name for a date-type field and sets its value, if one is provided, or returns a L<DateTime> object.
1688              
1689             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.
1690              
1691             =head2 _set_get_meta_value
1692              
1693             Takes a meta field name and sets or gets its value.
1694              
1695             =head1 AUTHOR
1696              
1697             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
1698              
1699             =head1 SEE ALSO
1700              
1701             L<Text::PO::Element>, L<Text::PO::MO>, L<Text::PO::Gettext>
1702              
1703             L<https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html>,
1704              
1705             L<https://en.wikipedia.org/wiki/Gettext>
1706              
1707             L<GNU documentation on header format|https://www.gnu.org/software/gettext/manual/html_node/Header-Entry.html>
1708              
1709             =head1 COPYRIGHT & LICENSE
1710              
1711             Copyright (c) 2020-2021 DEGUEST Pte. Ltd.
1712              
1713             You can use, copy, modify and redistribute this package and associated files under the same terms as Perl itself.
1714              
1715             =cut