File Coverage

blib/lib/Data/MuForm/Localizer.pm
Criterion Covered Total %
statement 131 186 70.4
branch 73 142 51.4
condition 2 3 66.6
subroutine 17 20 85.0
pod 0 15 0.0
total 223 366 60.9


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