File Coverage

blib/lib/Locale/Maketext/Extract.pm
Criterion Covered Total %
statement 204 220 92.7
branch 57 86 66.2
condition 20 35 57.1
subroutine 37 42 88.1
pod 5 35 14.2
total 323 418 77.2


line stmt bran cond sub pod time code
1             package Locale::Maketext::Extract;
2             $Locale::Maketext::Extract::VERSION = '1.00';
3 6     6   115769 use strict;
  6         16  
  6         316  
4 6     6   5224 use Locale::Maketext::Lexicon();
  6         23  
  6         26031  
5              
6             # ABSTRACT: Extract translatable strings from source
7              
8              
9             our %Known_Plugins = (
10             perl => 'Locale::Maketext::Extract::Plugin::Perl',
11             yaml => 'Locale::Maketext::Extract::Plugin::YAML',
12             tt2 => 'Locale::Maketext::Extract::Plugin::TT2',
13             text => 'Locale::Maketext::Extract::Plugin::TextTemplate',
14             mason => 'Locale::Maketext::Extract::Plugin::Mason',
15             generic => 'Locale::Maketext::Extract::Plugin::Generic',
16             formfu => 'Locale::Maketext::Extract::Plugin::FormFu',
17             haml => 'Locale::Maketext::Extract::Plugin::Haml',
18             );
19              
20             sub new {
21 10     10 0 280412 my $class = shift;
22 10         42 my %params = @_;
23             my $plugins = delete $params{plugins}
24 10   100     87 || { map { $_ => undef } keys %Known_Plugins };
25              
26 10         624 Locale::Maketext::Lexicon::set_option( 'keep_fuzzy' => 1 );
27 10         175 my $self = bless(
28             { header => '',
29             entries => {},
30             compiled_entries => {},
31             lexicon => {},
32             warnings => 0,
33             verbose => 0,
34             wrap => 0,
35             %params,
36             },
37             $class
38             );
39 10   50     112 $self->{verbose} ||= 0;
40 10 50       37 die "No plugins defined in new()"
41             unless $plugins;
42 10         53 $self->plugins($plugins);
43 10         63 return $self;
44             }
45              
46              
47 134 100   134 0 11106 sub header { $_[0]{header} || _default_header() }
48 160     160 0 457 sub set_header { $_[0]{header} = $_[1] }
49              
50 249     249 0 1009 sub lexicon { $_[0]{lexicon} }
51 160   100 160 0 2115 sub set_lexicon { $_[0]{lexicon} = $_[1] || {}; delete $_[0]{lexicon}{''}; }
  160         853  
52              
53 267     267 0 709 sub msgstr { $_[0]{lexicon}{ $_[1] } }
54 0     0 0 0 sub set_msgstr { $_[0]{lexicon}{ $_[1] } = $_[2] }
55              
56 343     343 0 1188 sub entries { $_[0]{entries} }
57 158   50 158 0 839 sub set_entries { $_[0]{entries} = $_[1] || {} }
58              
59 338     338 0 1209 sub compiled_entries { $_[0]{compiled_entries} }
60 160   100 160 0 2032 sub set_compiled_entries { $_[0]{compiled_entries} = $_[1] || {} }
61              
62 0 0   0 0 0 sub entry { @{ $_[0]->entries->{ $_[1] } || [] } }
  0         0  
63 185     185 0 241 sub add_entry { push @{ $_[0]->entries->{ $_[1] } }, $_[2] }
  185         571  
64 0     0 0 0 sub del_entry { delete $_[0]->entries->{ $_[1] } }
65              
66 180 100   180 0 199 sub compiled_entry { @{ $_[0]->compiled_entries->{ $_[1] } || [] } }
  180         403  
67 0     0 0 0 sub add_compiled_entry { push @{ $_[0]->compiled_entries->{ $_[1] } }, $_[2] }
  0         0  
68 0     0 0 0 sub del_compiled_entry { delete $_[0]->compiled_entries->{ $_[1] } }
69              
70             sub plugins {
71 183     183 1 290 my $self = shift;
72 183 100       514 if (@_) {
73 10         18 my @plugins;
74 10         17 my %params = %{ shift @_ };
  10         70  
75              
76 10         43 foreach my $name ( keys %params ) {
77 38   66     179 my $plugin_class = $Known_Plugins{$name} || $name;
78 38         108 my $filename = $plugin_class . '.pm';
79 38         233 $filename =~ s/::/\//g;
80 38         66 local $@;
81             eval {
82 38 50       510396 require $filename && 1;
83 38         226 1;
84 38 50       74 } or do {
85 0   0     0 my $error = $@ || 'Unknown';
86 0 0       0 print STDERR "Error loading $plugin_class: $error\n"
87             if $self->{warnings};
88 0         0 next;
89             };
90              
91 38 100       608 my $plugin
92             = $params{$name}
93             ? $plugin_class->new( $params{$name} )
94             : $plugin_class->new;
95 38         156 push @plugins, $plugin;
96             }
97 10         76 $self->{plugins} = \@plugins;
98             }
99 183   50     815 return $self->{plugins} || [];
100             }
101              
102             sub clear {
103 158     158 0 119272 $_[0]->set_header;
104 158         541 $_[0]->set_lexicon;
105 158         528 $_[0]->set_comments;
106 158         388 $_[0]->set_fuzzy;
107 158         573 $_[0]->set_entries;
108 158         569 $_[0]->set_compiled_entries;
109             }
110              
111              
112             sub read_po {
113 2     2 1 672 my ( $self, $file ) = @_;
114 2 50       8 print STDERR "READING PO FILE : $file\n"
115             if $self->{verbose};
116              
117 2         3 my $header = '';
118              
119 2         7 local ( *LEXICON, $_ );
120 2 50       125 open LEXICON, $file or die $!;
121 2         765 while () {
122 24 100       58 ( 1 .. /^$/ ) or last;
123 22         44 $header .= $_;
124             }
125 2         11 1 while chomp $header;
126              
127 2         17 $self->set_header("$header\n");
128              
129 2         631 require Locale::Maketext::Lexicon::Gettext;
130 2         6 my $lexicon = {};
131 2         4 my $comments = {};
132 2         4 my $fuzzy = {};
133 2         9 $self->set_compiled_entries( {} );
134              
135 2 50       7 if ( defined($_) ) {
136 2         123 ( $lexicon, $comments, $fuzzy )
137             = Locale::Maketext::Lexicon::Gettext->parse( $_, );
138             }
139              
140             # Internally the lexicon is in gettext format already.
141 2         15 $self->set_lexicon( { map _maketext_to_gettext($_), %$lexicon } );
142 2         7 $self->set_comments($comments);
143 2         5 $self->set_fuzzy($fuzzy);
144              
145 2         31 close LEXICON;
146             }
147              
148             sub msg_comment {
149 93     93 0 151 my $self = shift;
150 93         134 my $msgid = shift;
151 93         225 my $comment = $self->{comments}->{$msgid};
152 93         341 return $comment;
153             }
154              
155             sub msg_fuzzy {
156 94 100   94 0 413 return $_[0]->{fuzzy}{ $_[1] } ? ', fuzzy' : '';
157             }
158              
159             sub set_comments {
160 160     160 0 475 $_[0]->{comments} = $_[1];
161             }
162              
163             sub set_fuzzy {
164 160     160 0 351 $_[0]->{fuzzy} = $_[1];
165             }
166              
167              
168             sub write_po {
169 68     68 1 352 my ( $self, $file, $add_format_marker ) = @_;
170 68 50       305 print STDERR "WRITING PO FILE : $file\n"
171             if $self->{verbose};
172              
173 68         201 local *LEXICON;
174 68 50       9556 open LEXICON, ">$file" or die "Can't write to $file$!\n";
175              
176 68         329 print LEXICON $self->header;
177              
178 68         255 foreach my $msgid ( $self->msgids ) {
179 90         290 $self->normalize_space($msgid);
180 90         187 print LEXICON "\n";
181 90 100       266 if ( my $comment = $self->msg_comment($msgid) ) {
182 1         5 my @lines = split "\n", $comment;
183 1         3 print LEXICON map {"# $_\n"} @lines;
  1         5  
184             }
185 90         292 print LEXICON $self->msg_variables($msgid);
186 90         302 print LEXICON $self->msg_positions($msgid);
187 90         284 my $flags = $self->msg_fuzzy($msgid);
188 90 100       244 $flags .= $self->msg_format($msgid) if $add_format_marker;
189 90 100       199 print LEXICON "#$flags\n" if $flags;
190 90         261 print LEXICON $self->msg_out($msgid);
191             }
192              
193 68 50       4608 print STDERR "DONE\n\n"
194             if $self->{verbose};
195              
196             }
197              
198              
199             sub extract {
200 159     159 0 5562 my $self = shift;
201 159         241 my $file = shift;
202 159         230 my $content = shift;
203              
204 159         220 local $@;
205              
206 159         256 my ( @messages, $total, $error_found );
207 159         237 $total = 0;
208 159         316 my $verbose = $self->{verbose};
209              
210 159         488 my @plugins = $self->_plugins_specifically_for_file($file);
211              
212             # If there's no plugin which can handle this file
213             # specifically, fall back trying with all known plugins.
214 159 50       397 @plugins = @{ $self->plugins } if not @plugins;
  159         412  
215              
216 159         342 foreach my $plugin (@plugins) {
217 187         616 pos($content) = 0;
218 187         422 my $success = eval { $plugin->extract($content); 1; };
  187         869  
  181         4641  
219 187 100       10928 if ($success) {
220 181         746 my $entries = $plugin->entries;
221 181 50 33     1050 if ( $verbose > 1 && @$entries ) {
222 0         0 push @messages,
223             " - "
224             . ref($plugin)
225             . ' - Strings extracted : '
226             . ( scalar @$entries );
227             }
228 181         439 for my $entry (@$entries) {
229 185         503 my ( $string, $line, $vars ) = @$entry;
230 185         811 $self->add_entry( $string => [ $file, $line, $vars ] );
231 185 50       725 if ( $verbose > 2 ) {
232 0 0       0 $vars = '' if !defined $vars;
233              
234             # pad string
235 0         0 $string =~ s/\n/\n /g;
236 0         0 push @messages,
237             sprintf(
238             qq[ - %-8s "%s" (%s)],
239             $line . ':',
240             $string, $vars
241             ),
242             ;
243             }
244             }
245 181         479 $total += @$entries;
246             }
247             else {
248 6         12 $error_found++;
249 6 50       35 if ( $self->{warnings} ) {
250 0         0 push @messages,
251             "Error parsing '$file' with plugin "
252             . ( ref $plugin )
253             . ": \n $@\n";
254             }
255             }
256 187         767 $plugin->clear;
257             }
258              
259 159 0 0     523 print STDERR " * $file\n - Total strings extracted : $total"
    0 33        
260             . ( $error_found ? ' [ERROR ] ' : '' ) . "\n"
261             if $verbose
262             && ( $total || $error_found );
263 159 50       1078 print STDERR join( "\n", @messages ) . "\n"
264             if @messages;
265              
266             }
267              
268             sub extract_file {
269 2     2 0 5 my ( $self, $file ) = @_;
270              
271 2         5 local ( *FH );
272 2 50       101 open FH, $file or die "Error reading from file '$file' : $!";
273 2         2 my $content = do {
274 2         7 local $/;
275 2         40 scalar ;
276             };
277              
278 2         9 $self->extract( $file => $content );
279 2         44 close FH;
280             }
281              
282              
283             sub compile {
284 158     158 1 814 my ( $self, $entries_are_in_gettext_style ) = @_;
285 158         337 my $entries = $self->entries;
286 158         479 my $lexicon = $self->lexicon;
287 158         418 my $comp = $self->compiled_entries;
288              
289 158         754 while ( my ( $k, $v ) = each %$entries ) {
290 177 100       573 my $compiled_key = (
291             ($entries_are_in_gettext_style)
292             ? $k
293             : _maketext_to_gettext($k)
294             );
295 177         445 $comp->{$compiled_key} = $v;
296 177 50       1027 $lexicon->{$compiled_key} = ''
297             unless exists $lexicon->{$compiled_key};
298             }
299              
300 158         900 return %$lexicon;
301             }
302              
303              
304             my %Escapes = map { ( "\\$_" => eval("qq(\\$_)") ) } qw(t r f b a e);
305              
306             sub normalize_space {
307 90     90 1 148 my ( $self, $msgid ) = @_;
308 90         120 my $nospace = $msgid;
309 90         274 $nospace =~ s/ +$//;
310              
311             return
312 90 50 66     228 unless ( !$self->has_msgid($msgid) and $self->has_msgid($nospace) );
313              
314 0         0 $self->set_msgstr( $msgid => $self->msgstr($nospace)
315             . ( ' ' x ( length($msgid) - length($nospace) ) ) );
316             }
317              
318              
319 68     68 0 100 sub msgids { sort keys %{ $_[0]{lexicon} } }
  68         478  
320              
321             sub has_msgid {
322 177     177 0 552 my $msg_str = $_[0]->msgstr( $_[1] );
323 177 50       991 return defined $msg_str ? length $msg_str : 0;
324             }
325              
326             sub msg_positions {
327 90     90 0 146 my ( $self, $msgid ) = @_;
328 90         185 my %files = ( map { ( " $_->[0]:$_->[1]" => 1 ) }
  95         551  
329             $self->compiled_entry($msgid) );
330 2         9 return $self->{wrap}
331 90 100       623 ? join( "\n", ( map { '#:' . $_ } sort( keys %files ) ), '' )
332             : join( '', '#:', sort( keys %files ), "\n" );
333             }
334              
335             sub msg_variables {
336 90     90 0 146 my ( $self, $msgid ) = @_;
337 90         119 my $out = '';
338              
339 90         105 my %seen;
340 90         311 foreach my $entry ( grep { $_->[2] } $self->compiled_entry($msgid) ) {
  95         347  
341 56         154 my ( $file, $line, $var ) = @$entry;
342 56         156 $var =~ s/^\s*,\s*//;
343 56         341 $var =~ s/\s*$//;
344 56 100 66     620 $out .= "#. ($var)\n" unless !length($var) or $seen{$var}++;
345             }
346              
347 90         299 return $out;
348             }
349              
350             sub msg_format {
351 3     3 0 4 my ( $self, $msgid ) = @_;
352 3 100       11 return ", perl-maketext-format"
353             if $msgid =~ /%(?:[1-9]\d*|\w+\([^\)]*\))/;
354 2         16 return '';
355             }
356              
357             sub msg_out {
358 90     90 0 136 my ( $self, $msgid ) = @_;
359 90         196 my $msgstr = $self->msgstr($msgid);
360              
361 90         307 return "msgid " . _format($msgid) . "msgstr " . _format($msgstr);
362             }
363              
364              
365             sub _default_header {
366 133     133   1305 return << '.';
367             # SOME DESCRIPTIVE TITLE.
368             # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
369             # This file is distributed under the same license as the PACKAGE package.
370             # FIRST AUTHOR , YEAR.
371             #
372             #, fuzzy
373             msgid ""
374             msgstr ""
375             "Project-Id-Version: PACKAGE VERSION\n"
376             "POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
377             "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
378             "Last-Translator: FULL NAME \n"
379             "Language-Team: LANGUAGE \n"
380             "MIME-Version: 1.0\n"
381             "Content-Type: text/plain; charset=CHARSET\n"
382             "Content-Transfer-Encoding: 8bit\n"
383             .
384             }
385              
386             sub _maketext_to_gettext {
387 183     183   240 my $text = shift;
388 183 50       1180 return '' unless defined $text;
389              
390 183         402 $text =~ s{((?
391             {$1%$2}g;
392 183         369 $text =~ s{((?
393 4         22 {"$1%$2(" . _escape($3) . ')'}eg;
394              
395 183         323 $text =~ s/~([\~\[\]])/$1/g;
396 183         411 return $text;
397             }
398              
399             sub _escape {
400 4     4   12 my $text = shift;
401 4         32 $text =~ s/\b_([1-9]\d*)/%$1/g;
402 4         16 return $text;
403             }
404              
405             sub _format {
406 180     180   239 my $str = shift;
407              
408 180         380 $str =~ s/(?=[\\"])/\\/g;
409              
410 180         735 while ( my ( $char, $esc ) = each %Escapes ) {
411 1080         9573 $str =~ s/$esc/$char/g;
412             }
413              
414 180 100       1062 return "\"$str\"\n" unless $str =~ /\n/;
415 26         103 my $multi_line = ( $str =~ /\n(?!\z)/ );
416 26         136 $str =~ s/\n/\\n"\n"/g;
417 26 100       116 if ( $str =~ /\n"$/ ) {
418 23         53 chop $str;
419             }
420             else {
421 3         6 $str .= "\"\n";
422             }
423 26 100       124 return $multi_line ? qq(""\n"$str) : qq("$str);
424             }
425              
426             sub _plugins_specifically_for_file {
427 171     171   13171 my ( $self, $file ) = @_;
428              
429 171 100       637 return () if not $file;
430              
431 112         145 my @plugins = grep {
432 14         38 my $plugin = $_;
433 112         1204 my @file_types = $plugin->file_types;
434 112   100     384 my $is_generic
435             = ( scalar @file_types == 1 and $file_types[0] eq '*' );
436 112 100       467 ( not $is_generic and $plugin->known_file_type($file) );
437 14         19 } @{ $self->plugins };
438              
439 14         47 return @plugins;
440             }
441              
442             1;
443              
444             __END__