File Coverage

blib/lib/Locale/PO/Callback.pm
Criterion Covered Total %
statement 131 167 78.4
branch 32 58 55.1
condition 6 9 66.6
subroutine 13 18 72.2
pod 7 7 100.0
total 189 259 72.9


line stmt bran cond sub pod time code
1             package Locale::PO::Callback;
2              
3             # TO DO:
4             # - support encodings other than UTF-8
5              
6 6     6   91392 use strict;
  6         15  
  6         270  
7 6     6   33 use warnings;
  6         9  
  6         189  
8              
9 6     6   5668 use POSIX qw(strftime);
  6         103474  
  6         50  
10              
11             our $VERSION = 0.04;
12              
13             sub new {
14 4     4 1 1232 my ($class, $callback) = @_;
15 4         13 my $self = {
16             callback => $callback,
17             };
18              
19 4         21 return bless($self, $class);
20             }
21              
22             sub read_string {
23 0     0 1 0 my ($self, $string) = @_;
24              
25 0         0 $self->read($string, 1);
26             }
27              
28             sub read_file {
29 0     0 1 0 my ($self, $filename) = @_;
30              
31 0         0 $self->read($filename, 0);
32             }
33              
34             sub read {
35 3     3 1 30 my ($self, $filename, $is_string) = @_;
36              
37 3 50       11 if ($is_string) {
38 0 0       0 open PO, '<', \$filename or die "Couldn't read string: $!";
39             } else {
40 3 50       231 open PO, "<$filename" or die "Couldn't open $filename: $!";
41             }
42              
43 3         22 binmode PO, ":utf8";
44              
45             # Every line in a .po file is either:
46             # * a comment, applying to the next stanza
47             # (Some comments may have machine-readable meanings,
48             # but they remain comments)
49             # * a command, part of a stanza
50             # * a continuation of a previous command
51             # * something else, which should be passed through
52             #
53             # The first stanza may map the empty string to a non-empty
54             # string, and if so it is magic and defines headers.
55              
56             my $empty_stanza = sub {
57             {
58 18     18   227 comments => '',
59             };
60 3         17 };
61              
62             my $escaper = sub {
63 21     21   60 my %escapes = (
64             'n' => "\n",
65             't' => "\t",
66             );
67 21         39 my ($char) = @_;
68              
69 21 50       135 return $escapes{$char} if defined $escapes{$char};
70 0         0 return $char;
71 3         29 };
72              
73             my $unstring = sub {
74 51     51   167 my ($str) = @_;
75 51         348 $str =~ s/^"//;
76 51         610 $str =~ s/"$//;
77 51         168 $str =~ s/\\(.)/$escaper->($1)/ge;
  21         160  
78            
79 51         371 return $str;
80 3         19 };
81              
82 3         6 my $first = 1;
83              
84 3         10 my $stanza = $empty_stanza->();
85              
86             # Which command a continuation is a continuation of.
87 3         6 my $continuing = 'msgstr';
88              
89             my $handle_stanza = sub {
90 18 100   18   37 if ($first) {
91              
92 3 50       124 if ($stanza->{'msgid'} eq '') {
93             # good, that's what we expected
94 3         15 $stanza->{'type'} = 'header';
95 3         8 $stanza->{'headers'} = {};
96 3         10 $stanza->{'header_order'} = [];
97             # FIXME allow for continuation lines
98             # although nobody ever uses them
99 3         36 for my $line (split /\n/, $stanza->{'msgstr'}) {
100 21         99 $line =~ /^(.*): (.*)$/;
101 21         109 $stanza->{'headers'}->{lc $1} = $2;
102 21         28 push @{ $stanza->{'header_order'} }, $1;
  21         77  
103             }
104 3         13 delete $stanza->{'msgid'};
105 3         11 delete $stanza->{'msgstr'};
106             } else {
107             # Oh dear, no header. Fake one.
108 0         0 $self->{callback}->({
109             type => 'header',
110             headers => {},
111             header_order => [],
112             });
113             }
114              
115 3         30 $first = 0;
116             } else {
117 15         31 $stanza->{'type'} = 'translation';
118 15         31 $stanza->{'locations'} = [];
119 15         37 $stanza->{'flags'} = {};
120 15 50       41 $stanza->{'comments'} = '' unless defined $stanza->{'comments'};
121              
122 15         18 my @comments;
123              
124 15         57 for my $comment (split /\n/, $stanza->{'comments'}) {
125 9 100       74 if ($comment =~ /^#: (.*):(\d*)$/) {
    50          
126 3         13 push @{ $stanza->{'locations'} }, [$1, $2];
  3         17  
127             } elsif ($comment =~ /^#, (.*)$/) {
128 6         13 my $flags = $1;
129 6         15 $flags =~ s/\s*,\s*/,/g;
130 6         18 for my $flag (split m/,/, $flags) {
131 6         34 $stanza->{'flags'}->{lc $flag} = 1;
132             }
133             } else {
134 0         0 push @comments, $comment;
135             }
136             }
137              
138             # Anything we didn't handle goes back in the comments field.
139 15         43 $stanza->{'comments'} = join("\n", @comments);
140             }
141              
142 18 100 66     133 if (defined $stanza->{'msgid'} ||
      66        
      66        
143             defined $stanza->{'msgstr'} ||
144             defined $stanza->{'msgstr[0]'} ||
145             defined $stanza->{'headers'}
146             ) {
147 15         56 $self->{callback}->($stanza);
148             }
149 3         35 };
150              
151 3         108 while () {
152 78         179 chomp;
153 78 100       446 if (/^$/) {
    100          
    100          
    50          
154 15         29 $handle_stanza->();
155 15         334 $stanza = $empty_stanza->();
156             } elsif (/^#/) {
157 12         77 $stanza->{comments} .= $_ . "\n";
158             } elsif (/^"/) {
159 21         55 $stanza->{$continuing} .= $unstring->($_);
160             } elsif (/^([^ ]*) (".*)/) {
161 30         56 $stanza->{$1} = $unstring->($2);
162 30         264 $continuing = $1;
163             } else {
164 0         0 $self->{callback}->({other => $_, type => 'other'});
165             }
166             }
167 3         10 $handle_stanza->();
168 3 50       107 close PO or die "Couldn't close $filename: $!";
169             }
170              
171             sub create_empty {
172 1     1 1 5 my ($self) = @_;
173              
174 1         7 my @fields = (
175             ['Project-Id-Version' => 'PACKAGE VERSION'],
176             ['PO-Revision-Date' => _today()],
177             # FIXME: take this from the environment,
178             # if it's available?
179             ['Last-Translator' => 'FULL NAME '],
180             ['Language-Team' => 'LANGUAGE '],
181             ['MIME-Version' => '1.0'],
182             ['Content-Type' => 'text/plain; charset=UTF-8'],
183             ['Content-Transfer-Encoding' => '8bit'],
184             );
185              
186 1         4 my @fieldnames;
187             my %fields;
188              
189 1         3 for (@fields) {
190 7         13 push @fieldnames, $_->[0];
191             }
192              
193 1         2 for (@fields) {
194 7         19 $fields{lc $_->[0]} = $_->[1];
195             }
196              
197 1         20 $self->{callback}->({type => 'header',
198             headers => \%fields,
199             header_order => \@fieldnames,
200             });
201             }
202              
203             sub rebuilder {
204 2     2 1 962 my ($callback) = @_;
205              
206 2 50   0   10 $callback = sub { print $_[0]; } unless $callback;
  0         0  
207              
208             return sub {
209 11     11   43 my ($stanza) = @_;
210              
211             my $output_line = sub {
212 32         40 my ($keyword) = @_;
213 32         59 my $text = $stanza->{$keyword};
214 32         38 my $max_width = 79;
215              
216 32 100       317 return '' unless defined $text;
217              
218 16         29 $text =~ s/\\/\\\\/g;
219 16         24 $text =~ s/\t/\\t/g;
220 16         30 $text =~ s/\n/\\n/g;
221 16         22 $text =~ s/\"/\\"/g;
222              
223             # Test the simple case first
224 16 50       64 if (length($keyword) + 4 + length($text) <= $max_width) {
225 16         83 return "$keyword \"$text\"\n";
226             }
227              
228 0         0 my $result = "$keyword \"\"\n";
229              
230 0         0 my @words;
231              
232 0         0 while ($text) {
233 0         0 $text =~ s/^(\S*\s*)//;
234 0         0 push @words, $1;
235             }
236              
237 0         0 my $temp = '';
238            
239 0         0 for (@words) {
240 0 0       0 if (length($temp . $_) >= $max_width) {
241 0 0       0 if ($temp) {
242 0         0 $result .= "\"$temp\"\n";
243 0         0 $temp = $_;
244             } else {
245 0         0 $result .= "\"$_\"\n";
246             }
247             } else {
248 0         0 $temp .= $_;
249             }
250             }
251              
252 0 0       0 $result .= "\"$temp\"\n" if $temp;
253            
254 0         0 return $result;
255 11         83 };
256              
257 11         20 my $result = '';
258              
259 11 100       39 if ($stanza->{'type'} eq 'translation') {
    50          
    0          
260 8 50       20 $result .= $stanza->{'comments'}."\n" if $stanza->{'comments'};
261 8         10 for my $flag (keys %{$stanza->{'flags'}}) {
  8         27  
262 4         16 $result .= "#, $flag\n";
263             }
264 8         14 for my $location (@{$stanza->{'locations'}}) {
  8         20  
265 2         12 $result .= "#: $location->[0]:$location->[1]\n";
266             }
267 8         18 $result .= $output_line->('msgctxt');
268 8         17 $result .= $output_line->('msgid');
269 8         239 $result .= $output_line->('msgid_plural');
270 8         54 for my $msgstr (grep { /^msgstr/ } sort keys %$stanza) {
  48         109  
271 8         18 $result .= $output_line->($msgstr);
272             }
273 8         21 $result .= "\n";
274             } elsif ($stanza->{'type'} eq 'header') {
275 3 100       14 $result .= $stanza->{'comments'} if $stanza->{'comments'};
276 3         9 $result .= "msgid \"\"\n";
277 3         8 $result .= "msgstr \"\"\n";
278 3         5 my %seen;
279 3         4 for my $header (@{$stanza->{'header_order'}}) {
  3         10  
280 21         50 my $value = $stanza->{'headers'}->{lc $header};
281 21         51 $result .= "\"$header: $value\\n\"\n";
282 21         66 $seen{lc $header} = 1;
283             }
284 3         8 for my $header (keys %{$stanza->{'headers'}}) {
  3         22  
285 22         37 $header = lc $header;
286 22 100       63 next if defined $seen{$header};
287 1         3 my $value = $stanza->{'headers'}->{$header};
288 1         8 $header =~ s/\b(.)/\u$1/g; # titlecase
289 1         6 $result .= "\"$header: $value\\n\"\n";
290             }
291 3         16 $result .= "\n";
292             } elsif ($stanza->{'type'} eq 'other') {
293 0         0 $result .= '# (other:) '.$stanza->{'other'}."\n";
294             } else {
295 0         0 die "Unknown type $stanza->{'type'}";
296             }
297              
298 11         36 $callback->($result);
299 2         26 };
300             }
301              
302             sub _today {
303 1     1   382 return strftime("%Y-%m-%d %H:%M %z", localtime);
304             }
305              
306             sub set_date {
307 0     0 1   my ($callback) = @_;
308              
309             return sub {
310 0     0     my ($param) = @_;
311              
312 0 0         if ($param->{'type'} eq 'header') {
313 0           my $date_found = defined $param->{'headers'}->{'po-revision-date'};
314              
315 0           $param->{'headers'}->{'po-revision-date'} = _today();
316              
317 0 0         push @{$param->{'header_order'}}, 'PO-Revision-Date'
  0            
318             unless $date_found;
319             }
320              
321 0           $callback->($param);
322 0           };
323             }
324              
325             1;
326              
327             =head1 NAME
328              
329             Locale::PO::Callback - parse gettext source files
330              
331             =head1 AUTHOR
332              
333             Thomas Thurman
334              
335             =head1 SYNOPSIS
336              
337             use Locale::PO::Callback;
338              
339             sub callback {
340             # ...
341             }
342              
343             my $lpc = Locale::PO::Callback->new(\&callback);
344             $lpc->read('test.po');
345              
346             =head1 DESCRIPTION
347              
348             This module parses the .po files used by GNU gettext
349             to hold translation catalogues. It takes one parameter,
350             a coderef, and calls it repeatedly with a description of
351             every item in the file. This enables chains of filters
352             to be produced, as is commonly done with XML processing.
353              
354             =head1 METHODS
355              
356             =head2 new(callback)
357              
358             Creates an object. The callback parameter is a coderef
359             which will be called with a description of every item
360             in the file.
361              
362             =head2 read_file(filename)
363              
364             Reads and parses a file.
365              
366             =head2 read_string(string)
367              
368             Parses a string.
369              
370             =head2 read(filename_or_string, is_string)
371              
372             Reads and parses a file or a string, depending on the is_string
373             argument.
374              
375             =head2 create_empty()
376              
377             Behaves as though we had just read in an empty file,
378             with default headers.
379              
380             =head1 OTHER THINGS
381              
382             =head2 rebuilder(coderef)
383              
384             Given a coderef, this function returns a function which
385             can be passed as a callback to this class's constructor.
386             The coderef will be called with strings which, if concatenated,
387             make a .po file equivalent to the source .po file.
388              
389             In pipeline terms, this function produces sinks.
390              
391             =head2 set_date(coderef)
392              
393             Given a coderef, this function returns a function which
394             can be passed as a callback to this class's constructor.
395             The function will pass its parameters through to the
396             coderef unchanged, except for headers, when the file date
397             will be changed to the current system date.
398              
399             In pipeline terms, this function produces filters.
400              
401             =head1 PARAMETERS TO THE CALLBACK
402              
403             =head2 type
404              
405             "header", "translation", or "other" (which last should never
406             appear in ordinary use).
407              
408             =head2 comments
409              
410             An arrayref of comments which appear before this item.
411              
412             =head2 flags
413              
414             A hashref of the flags of this item (such as "fuzzy").
415              
416             =head2 locations
417              
418             An arrayref of arrayrefs, the first item being a filename
419             and the second being a line number.
420              
421             =head2 msgid
422              
423             The source message, in its singular form.
424              
425             =head2 msgid_plural
426              
427             The source message, in its plural form.
428             This is usually empty.
429              
430             =head2 msgstr
431              
432             The translation, if any,
433             unless this translation has plural forms,
434             in which case see the next entry.
435              
436             =head2 msgstr[0] (etc)
437              
438             Variations on the translation for different plural forms.
439              
440             =head2 msgctxt
441              
442             The "context" of the translation.
443             Rarely filled in.
444              
445             =head2 headers
446              
447             A hashref of headers, mapping fieldnames to values.
448             The keys are lowercased.
449              
450             =head2 header_order
451              
452             An arrayref of the header fieldnames, in the casing and order
453             in which they were found.
454              
455             =head1 FUTURE EXPANSION
456              
457             We need to support encodings other than UTF-8.
458              
459             This documentation was written in a bit of a rush.
460              
461             =head1 COPYRIGHT
462              
463             This Perl module is copyright (C) Thomas Thurman, 2010.
464             This is free software, and can be used/modified under the same terms as
465             Perl itself.
466              
467