File Coverage

blib/lib/Data/MuForm/Localizer.pm
Criterion Covered Total %
statement 125 176 71.0
branch 72 142 50.7
condition 2 3 66.6
subroutine 16 18 88.8
pod 0 13 0.0
total 215 352 61.0


line stmt bran cond sub pod time code
1             package Data::MuForm::Localizer;
2             # ABSTRACT: Localizer
3 94     94   2869 use Moo;
  94         18546  
  94         515  
4              
5 94     94   22389 use Types::Standard -types;
  94         98698  
  94         818  
6              
7              
8             has 'language' => ( is => 'rw', builder => 'build_language' );
9 0     0 0 0 sub build_language { 'en' }
10              
11             has 'messages_directory' => ( is => 'rw' );
12              
13             sub loc_ {
14 251     251 0 5664 my ($self, $msgid) = @_;
15             # translate($self, $msgctxt, $msgid, $msgid_plural, $count, $is_n)
16 251         544 return $self->translate(undef, $msgid);
17             }
18              
19             sub loc_x {
20 41     41 0 4294 my ($self, $msgid, %args) = @_;
21             # translate($self, $msgctxt, $msgid, $msgid_plural, $count, $is_n)
22 41         132 my $msg = $self->translate(undef, $msgid);
23 41         159 my $out = $self->expand_named( $msg, %args );
24 41         148 return $out;
25             }
26              
27             sub loc_nx {
28 1     1 0 9 my ($self, $msgid, $msgid_plural, $count, %args) = @_;
29             # translate($self, $msgctxt, $msgid, $msgid_plural, $count, $is_n)
30 1         4 my $msg = $self->translate(undef, $msgid, $msgid_plural, $count, 1);
31 1         4 my $out = $self->expand_named( $msg, %args );
32 1         4 return $out;
33             }
34              
35              
36             our $lexicons = {
37             };
38              
39             sub get_lexicon {
40 294     294 0 1621 my $self = shift;
41 294         596 my $lang = $self->language;
42 294 100       630 if ( ! exists $lexicons->{$lang} ) {
43 40         145 $lexicons->{$lang} = $self->load_lexicon($lang);
44             }
45 294         414 return $lexicons->{$lang};
46             }
47              
48             sub load_lexicon {
49 40     40 0 67 my ( $self, $lang ) = @_;
50              
51 40         113 my $file = "lib/Data/MuForm/Messages/$lang.po";
52 40         139 my $lexicon = $self->load_file($file);
53 40         137 return $lexicon;
54             }
55              
56             sub load_file {
57 40     40 0 63 my ( $self, $file ) = @_;
58              
59 40 50   40   979 open(IN, "<:encoding(UTF-8)", $file)
  40         208  
  40         49  
  40         251  
60             or return undef;
61              
62 40         270697 my @entries;
63             my %entries;
64 0         0 my $index;
65 0         0 my $po;
66 0         0 my %buffer;
67 0         0 my $last_buffer;
68 40         87 my $line_number = 0;
69 40         831 while (<IN>) {
70 8560         11499 chomp;
71 8560         5017 $line_number++;
72              
73             # Strip trailing \r\n chars
74 8560         37755 s{[\r\n]*$}{};
75              
76 8560 100 66     49393 if (/^$/) {
    100          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    100          
    100          
    100          
    100          
    50          
77              
78             # Empty line. End of an entry.
79              
80 2040 50       2645 if (defined($po)) {
81             $po->{fuzzy_msgctxt} = $buffer{fuzzy_msgctxt}
82 2040 50       2727 if defined $buffer{fuzzy_msgctxt};
83 2040 50       2342 $po->{fuzzy_msgid} = $buffer{fuzzy_msgid} if defined $buffer{fuzzy_msgid};
84             $po->{fuzzy_msgid_plural} = $buffer{fuzzy_msgid_plural}
85 2040 50       2299 if defined $buffer{fuzzy_msgid_plural};
86 2040 50       2324 $po->{msgctxt} = $buffer{msgctxt} if defined $buffer{msgctxt};
87 2040 50       3305 $po->{msgid} = $buffer{msgid} if defined $buffer{msgid};
88 2040 100       2467 $po->{msgid_plural} = $buffer{msgid_plural} if defined $buffer{msgid_plural};
89 2040 100       2952 $po->{msgstr} = $buffer{msgstr} if defined $buffer{msgstr};
90 2040 100       2420 $po->{msgstr_n} = $buffer{msgstr_n} if defined $buffer{msgstr_n};
91              
92             # Save this message
93 2040         3769 $entries{$po->{msgid}} = $po;
94 2040         1833 push @entries, $po;
95 2040         3864 my $index_key = join_message_key( msgid => $po->{msgid}, msgstr => $po->{msgstr}, msgctxt => $po->{msgctxt});
96 2040         4236 $index->{$index_key} = $po;
97              
98 2040         1431 $po = undef;
99 2040         1628 $last_buffer = undef;
100 2040         5517 %buffer = ();
101             }
102             }
103             elsif (/^#\s+(.*)/ or /^#()$/) {
104              
105             # Translator comments
106 2000 50       4001 $po = { line_number => $line_number } unless defined($po);
107 2000 50       2404 if (defined($po->{comment})) {
108 0         0 $po->{comment} = $po->{comment} . "\n$1";
109             }
110             else {
111 2000         5825 $po->{comment} = $1;
112             }
113             }
114             elsif (/^#\.\s*(.*)/) {
115              
116             # Automatic comments
117 0 0       0 $po = { line_number => $line_number } unless defined($po);
118 0 0       0 if (defined($po->{automatic})) {
119 0         0 $po->{automatic} = $po->automatic . "\n$1";
120             }
121             else {
122 0         0 $po->{automatic} = $1;
123             }
124             }
125             elsif (/^#:\s+(.*)/) {
126              
127             # reference
128 0 0       0 $po = { line_number => $line_number } unless defined($po);
129 0 0       0 if (defined($po->{reference})) {
130 0         0 $po->{reference} = $po->reference . "\n$1";
131             }
132             else {
133 0         0 $po->{reference} = $1;
134             }
135             }
136             elsif (/^#,\s+(.*)/) {
137              
138             # flags
139 0         0 my @flags = split /\s*[,]\s*/, $1;
140             # $po = { line_number => $line_number } unless defined($po);
141             # foreach my $flag (@flags) {
142             # $po->add_flag($flag);
143             # }
144             }
145             elsif (/^#(~)?\|\s+msgctxt\s+(.*)/) {
146 0 0       0 $po = { line_number => $line_number } unless defined($po);
147 0         0 $buffer{fuzzy_msgctxt} = $self->dequote($2);
148 0         0 $last_buffer = \$buffer{fuzzy_msgctxt};
149 0 0       0 $po->{obsolete} = 1 if $1;
150             }
151             elsif (/^#(~)?\|\s+msgid\s+(.*)/) {
152 0 0       0 $po = { line_number => $line_number } unless defined($po);
153 0         0 $buffer{fuzzy_msgid} = $self->dequote($2);
154 0         0 $last_buffer = \$buffer{fuzzy_msgid};
155 0 0       0 $po->{obsolete} = 1 if $1;
156             }
157             elsif (/^#(~)?\|\s+msgid_plural\s+(.*)/) {
158 0 0       0 $po = { line_number => $line_number } unless defined($po);
159 0         0 $buffer{fuzzy_msgid_plural} = $self->dequote($2);
160 0         0 $last_buffer = \$buffer{fuzzy_msgid_plural};
161 0 0       0 $po->{obsolete} = 1 if $1;
162             }
163             elsif (/^(#~\s+)?msgctxt\s+(.*)/) {
164 0 0       0 $po = { line_number => $line_number } unless defined($po);
165 0         0 $buffer{msgctxt} = $self->dequote($2);
166 0         0 $last_buffer = \$buffer{msgctxt};
167 0 0       0 $po->obsolete(1) if $1;
168             }
169             elsif (/^(#~\s+)?msgid\s+(.*)/) {
170 2040 100       2737 $po = { line_number => $line_number } unless defined($po);
171 2040         2747 $buffer{msgid} = $self->dequote($2);
172 2040         1754 $last_buffer = \$buffer{msgid};
173 2040 50       6608 $po->obsolete(1) if $1;
174             }
175             elsif (/^(#~\s+)?msgid_plural\s+(.*)/) {
176 40 50       152 $po = { line_number => $line_number } unless defined($po);
177 40         113 $buffer{msgid_plural} = $self->dequote($2);
178 40         72 $last_buffer = \$buffer{msgid_plural};
179 40 50       204 $po->obsolete(1) if $1;
180             }
181             elsif (/^(?:#~\s+)?msgstr\s+(.*)/) {
182              
183             # translated string
184 2000         2457 $buffer{msgstr} = $self->dequote($1);
185 2000         4715 $last_buffer = \$buffer{msgstr};
186             }
187             elsif (/^(?:#~\s+)?msgstr\[(\d+)\]\s+(.*)/) {
188              
189             # translated string
190 80         161 $buffer{msgstr_n}{$1} = $self->dequote($2);
191 80         430 $last_buffer = \$buffer{msgstr_n}{$1};
192             }
193             elsif (/^(?:#(?:~|~\||\|)\s+)?(".*)/) {
194              
195             # continued string. Accounts for:
196             # normal : "string"
197             # obsolete : #~ "string"
198             # fuzzy : #| "string"
199             # fuzzy+obsolete : #~| "string"
200 360         549 $$last_buffer .= $self->dequote($1);
201             }
202             else {
203 0         0 warn "Strange line at $file line $line_number: [$_]\n";
204             }
205             }
206 40 50       155 if (defined($po)) {
207              
208             $po->{msgctxt} = $buffer{msgctxt}
209 0 0       0 if defined $buffer{msgctxt};
210             $po->{msgid} = $buffer{msgid}
211 0 0       0 if defined $buffer{msgid};
212             $po->{msgid_plural} = $buffer{msgid_plural}
213 0 0       0 if defined $buffer{msgid_plural};
214             $po->{msgstr} = $buffer{msgstr}
215 0 0       0 if defined $buffer{msgstr};
216             $po->{msgstr_n} = $buffer{msgstr_n}
217 0 0       0 if defined $buffer{msgstr_n};
218              
219             # save messages
220 0         0 $entries{$po->{msgid}} = $po;
221 0         0 push @entries, $po;
222 0         0 my $index_key = join_message_key( msgid => $po->{msgid}, msgstr => $po->{msgstr}, msgctxt => $po->{msgctxt});
223 0         0 $index->{$index_key} = $po;
224              
225             }
226 40         1162 close IN;
227              
228             # the first entry is the header. Extract information from the header msgstr
229 40         91 my $header_ref = $entries[0];
230 40         187 %{$header_ref} = (
231             msgid => $header_ref->{msgid},
232 40         88 %{ $self->extract_header_msgstr( $header_ref->{msgstr} ) },
  40         177  
233             );
234              
235 40         375 return $index;
236             }
237              
238             sub dequote {
239 4520     4520 0 5912 my ( $self, $string ) = @_;
240              
241             return undef
242 4520 50       5468 unless defined $string;
243              
244 4520         12968 $string =~ s/^"(.*)"/$1/;
245 4520         4898 $string =~ s/\\"/"/g;
246 4520         4278 $string =~ s/(?<!(\\))\\n/\n/g; # newline
247 4520         4096 $string =~ s/(?<!(\\))\\{2}n/\\n/g; # inline newline
248 4520         3833 $string =~ s/(?<!(\\))\\{3}n/\\\n/g; # \ followed by newline
249 4520         3783 $string =~ s/\\{4}n/\\\\n/g; # \ followed by inline newline
250 4520         3634 $string =~ s/\\\\(?!n)/\\/g; # all slashes not related to a newline
251 4520         6809 return $string;
252             }
253              
254             sub expand_named {
255 42     42 0 97 my ($self, $text, @args) = @_;
256              
257 42 50       98 defined $text
258             or return $text;
259 42 50       216 my $arg_ref = @args == 1
    50          
260             ? $args[0]
261             : {
262             @args % 2
263             ? die 'MuForm error Arguments expected pairwise'
264             : @args
265             };
266              
267 42         60 my $regex = join q{|}, map { quotemeta $_ } keys %{$arg_ref};
  57         152  
  42         103  
268              
269 42         1430 $text =~ s{
270             (
271             \{
272             ( $regex )
273             (?: [ ]* [:] ( [^\}]+ ) )?
274             \}
275             )
276             }
277             {
278 52         196 $self->_mangle_value($1, $arg_ref->{$2}, $3)
279             }xmsge;
280             ## use critic (EscapedMetacharacters)
281              
282 42         155 return $text;
283             }
284              
285             sub _mangle_value {
286 52     52   393 my ($self, $placeholder, $value, $attribute) = @_;
287              
288 52 50       131 defined $value
289             or return q{};
290 52 100       224 defined $attribute
291             or return $value;
292 2         6 return $value;
293             }
294              
295              
296             sub translate {
297 293     293 0 352 my ($self, $msgctxt, $msgid, $msgid_plural, $count, $is_n) = @_;
298              
299 293         569 my $lexicon = $self->get_lexicon;
300              
301 293         533 my $msg_key = join_message_key(
302             msgctxt => $msgctxt,
303             msgid => $msgid,
304             msgid_plural => $msgid_plural,
305             );
306              
307 293 100       577 if ( $is_n ) {
308             my $plural_code = $lexicon->{ q{} }->{plural_code}
309 1 50       3 or die qq{Plural-Forms not found in lexicon};
310 1 50       22 my $multiplural_index = ref $count eq 'ARRAY'
311             ? $self->_calculate_multiplural_index($count, $plural_code, $lexicon )
312             : $plural_code->($count);
313             my $msgstr_plural = exists $lexicon->{$msg_key}
314 1 50       4 ? $lexicon->{$msg_key}->{msgstr_plural}->[$multiplural_index] : ();
315 1 50       2 if ( ! defined $msgstr_plural ) { # fallback
316 1 50       16 $msgstr_plural = $plural_code->($count)
317             ? $msgid_plural
318             : $msgid;
319             }
320 1         3 return $msgstr_plural;
321             }
322              
323             my $msgstr = exists $lexicon->{$msg_key}
324             ? $lexicon->{$msg_key}->{msgstr}
325 292 100       635 : ();
326 292 100       507 if ( ! defined $msgstr ) { # fallback
327 231         245 $msgstr = $msgid;
328             }
329              
330 292         2140 return $msgstr;
331             }
332              
333             sub length_or_empty_list {
334 6999     6999 0 5072 my $thing = shift;
335 6999 100       13055 defined $thing or return;
336 2334 100       3743 length $thing or return;
337 2294         4364 return $thing;
338             }
339              
340             sub _calculate_multiplural_index {
341 0     0   0 my ($self, $count_ref, $plural_code, $lexicon) = @_;
342              
343             my $nplurals = $lexicon->{ q{} }->{multiplural_nplurals}
344 0 0       0 or die qq{X-Multiplural-Nplurals not found in lexicon"};
345 0 0       0 my @counts = @{$count_ref}
  0         0  
346             or die 'Count array is empty';
347 0         0 my $index = 0;
348 0         0 while (@counts) {
349 0         0 $index *= $nplurals;
350 0         0 my $count = shift @counts;
351 0         0 $index += $plural_code->($count);
352             }
353              
354 0         0 return $index;
355             }
356              
357             sub join_message_key {
358 2333     2333 0 5268 my ( %args ) = @_;
359              
360             my $key = join( '{MSG}',
361             (
362             join( '{PL}',
363             length_or_empty_list( $args{msgid} ),
364             length_or_empty_list( $args{msgid_plural} ),
365             )
366             ),
367             length_or_empty_list( $args{msgctxt} )
368 2333         2855 );
369             }
370              
371             #====== Header Extract =======
372             my $perlify_plural_forms_ref__code_ref = sub {
373             my $plural_forms_ref = shift;
374              
375             ${$plural_forms_ref} =~ s{ \b ( nplurals | plural | n ) \b }{\$$1}xmsg;
376              
377             return;
378             };
379              
380             my $nplurals__code_ref = sub {
381             my $plural_forms = shift;
382              
383             $perlify_plural_forms_ref__code_ref->(\$plural_forms);
384             my $code = <<"EOC";
385             my \$n = 0;
386             my (\$nplurals, \$plural);
387             $plural_forms;
388             \$nplurals;
389             EOC
390             my $nplurals = eval $code;
391              
392             return $nplurals;
393             };
394              
395             my $plural__code_ref = sub {
396             my $plural_forms = shift;
397              
398             return $plural_forms =~ m{ \b plural= ( [^;\n]+ ) }xms;
399             };
400              
401             my $plural_code__code_ref = sub {
402             my $plural_forms = shift;
403              
404             $perlify_plural_forms_ref__code_ref->(\$plural_forms);
405             my $code = <<"EOC";
406             sub {
407             my \$n = shift;
408              
409             my (\$nplurals, \$plural);
410             $plural_forms;
411              
412             return 0 + \$plural;
413             }
414             EOC
415             my $code_ref = eval $code;
416              
417             return $code_ref;
418             };
419              
420             sub extract_header_msgstr {
421 44     44 0 567 my ( $class, $header_msgstr ) = @_;
422              
423 44 100       173 defined $header_msgstr
424             or die 'Header is not defined';
425             ## no critic (ComplexRegexes)
426 43 100       532 my ( $plural_forms ) = $header_msgstr =~ m{
427             ^
428             Plural-Forms:
429             [ ]*
430             (
431             nplurals [ ]* [=] [ ]* \d+ [ ]* [;]
432             [ ]*
433             plural [ ]* [=] [ ]* [^;\n]+ [ ]* [;]?
434             [ ]*
435             )
436             $
437             }xms
438             or die 'Plural-Forms not found in header';
439             ## use critic (ComplexRegexes)
440 42 100       458 my ( $charset ) = $header_msgstr =~ m{
441             ^
442             Content-Type:
443             [^;]+ [;] [ ]*
444             charset [ ]* = [ ]*
445             ( [^ ]+ )
446             [ ]*
447             $
448             }xms
449             or die 'Content-Type with charset not found in header';
450 41         178 my ( $multiplural_nplurals ) = $header_msgstr =~ m{
451             ^ X-Multiplural-Nplurals: [ ]* ( \d+ ) [ ]* $
452             }xms;
453              
454             return {(
455 41 50       157 nplurals => $nplurals__code_ref->($plural_forms),
456             plural => $plural__code_ref->($plural_forms),
457             plural_code => $plural_code__code_ref->($plural_forms),
458             charset => $charset,
459             (
460             $multiplural_nplurals
461             ? ( multiplural_nplurals => $multiplural_nplurals )
462             : ()
463             ),
464             )};
465             }
466              
467             1;
468              
469             __END__
470              
471             =pod
472              
473             =encoding UTF-8
474              
475             =head1 NAME
476              
477             Data::MuForm::Localizer - Localizer
478              
479             =head1 VERSION
480              
481             version 0.03
482              
483             =head1 DESCRIPTION
484              
485             Localizer for Data::MuForm
486              
487             This code has mainly been borrowed from Locale::TextDomain::OO.
488             It requires UTF-8 and handles only .po files. It does not use
489             the various LOCALE flags for languages. Language must be set
490             on creation of the form. It does handle plurals.
491              
492             TODO: implement allowing specification of user-provided messages
493             file, and merging two message files
494              
495             sub loc_px {
496             my ($self, $msgid, %args) = @_;
497              
498             return $msgid;
499             }
500              
501             sub loc_npx {
502             my ($self, $msgid, $msgid_plural, $cout, %args) = @_;
503             # my ($self, $msgctxt, $msgid, $msgid_plural, $count, @args) = @_;
504             # my $translation = $self->translate($msgctxt, $msgid, $msgid_plural, $count, 1);
505              
506             return $msgid;
507             }
508              
509             =head1 NAME
510              
511             Data::MuForm::Localizer
512              
513             =head1 AUTHOR
514              
515             Gerda Shank
516              
517             =head1 COPYRIGHT AND LICENSE
518              
519             This software is copyright (c) 2017 by Gerda Shank.
520              
521             This is free software; you can redistribute it and/or modify it under
522             the same terms as the Perl 5 programming language system itself.
523              
524             =cut