File Coverage

blib/lib/Locale/VersionedMessages.pm
Criterion Covered Total %
statement 273 319 85.5
branch 108 150 72.0
condition 14 24 58.3
subroutine 26 27 96.3
pod 12 12 100.0
total 433 532 81.3


line stmt bran cond sub pod time code
1             package Locale::VersionedMessages;
2             # Copyright (c) 2010-2014 Sullivan Beck. All rights reserved.
3             # This program is free software; you can redistribute it and/or modify it
4             # under the same terms as Perl itself.
5              
6             ########################################################################
7              
8             require 5.008;
9 4     4   92288 use strict;
  4         8  
  4         187  
10 4     4   19 use warnings;
  4         8  
  4         654  
11              
12             our $VERSION;
13             $VERSION='0.94';
14              
15             ########################################################################
16             # METHODS
17             ########################################################################
18              
19             sub new {
20 4     4 1 59 my($class) = @_;
21              
22 4         34 my $self = {
23             'err' => '',
24             'set' => {},
25             'mess' => {},
26             'search' => [],
27             };
28              
29 4         17 bless $self, $class;
30              
31 4         18 return $self;
32             }
33              
34             sub version {
35 0     0 1 0 my($self) = @_;
36 0         0 return $VERSION;
37             }
38              
39             sub err {
40 46     46 1 188 my($self) = @_;
41 46         111 return $$self{'err'};
42             }
43              
44 4     4   19 no strict 'refs';
  4         10  
  4         1200  
45             sub set {
46 6     6 1 5251 my($self,@set) = @_;
47              
48 6         31 $$self{'err'} = '';
49 6 50       24 if (! @set) {
50 0         0 return sort keys %{ $$self{'set'} };
  0         0  
51             }
52              
53 6         17 foreach my $set (@set) {
54 6 50       44 if ($set !~ /^[a-zA-Z0-9_]+$/) {
55 0         0 $$self{'err'} = "Set must be alphanumeric/underscore: $set";
56 0         0 return;
57             }
58              
59 6         18 my $m = "Locale::VersionedMessages::Sets::$set";
60 6         516 eval "require $m";
61 6 100       1146 if ($@) {
62 1         3 chomp($@);
63 1         5 $$self{'err'} = "Unable to load set: $set: $@";
64 1         4 return;
65             }
66              
67 5         11 my $def_locale = ${ "${m}::DefaultLocale" };
  5         26  
68 5         11 my @all_locale = @{ "${m}::AllLocale" };
  5         28  
69 5         11 my %messages = %{ "${m}::Messages" };
  5         36  
70              
71 12         82 $$self{'set'}{$set} = { 'def_loc' => $def_locale,
72 5         39 'all_loc' => { map {$_,1} @all_locale },
73             'messages' => \%messages,
74             'search' => [],
75             };
76             }
77              
78 5         14 return;
79             }
80 4     4   22 use strict 'refs';
  4         13  
  4         1873  
81              
82             sub query_set_default {
83 1     1 1 87 my($self,$set) = @_;
84 1         3 $$self{'err'} = '';
85              
86 1 50       6 if (! exists $$self{'set'}{$set}) {
87 0         0 $$self{'err'} = "Set not loaded: $set";
88 0         0 return;
89             }
90              
91 1         7 return $$self{'set'}{$set}{'def_loc'};
92             }
93              
94             sub query_set_locales {
95 1     1 1 102 my($self,$set) = @_;
96 1         3 $$self{'err'} = '';
97              
98 1 50       596 if (! exists $$self{'set'}{$set}) {
99 0         0 $$self{'err'} = "Set not loaded: $set";
100 0         0 return;
101             }
102              
103 1         2 return sort keys %{ $$self{'set'}{$set}{'all_loc'} };
  1         16  
104             }
105              
106             sub query_set_msgid {
107 1     1 1 129 my($self,$set) = @_;
108 1         3 $$self{'err'} = '';
109              
110 1 50       5 if (! exists $$self{'set'}{$set}) {
111 0         0 $$self{'err'} = "Set not loaded: $set";
112 0         0 return;
113             }
114              
115 1         2 return sort keys %{ $$self{'set'}{$set}{'messages'} };
  1         9  
116             }
117              
118             sub search {
119 6     6 1 657 my($self,@locale) = @_;
120 6         11 $$self{'err'} = '';
121              
122 6         8 my $set;
123 6 100 100     39 if (@locale && exists $$self{'set'}{$locale[0]}) {
124 2         4 $set = shift(@locale);
125             }
126              
127 6 100 100     43 if ($set && @locale) {
    100          
    100          
128 1         5 $$self{'set'}{$set}{'search'} = [@locale];
129              
130             } elsif (@locale) {
131 3         545 $$self{'search'} = [@locale];
132              
133             } elsif ($set) {
134 1         3 $$self{'set'}{$set}{'search'} = [];
135              
136             } else {
137 1         2 $$self{'search'} = [];
138             }
139              
140 6         21 return;
141             }
142              
143             sub query_search {
144 6     6 1 24998 my($self,$set) = @_;
145 6         14 $$self{'err'} = '';
146              
147 6 100       60 if ($set) {
148 3 50       10 if (! exists $$self{'set'}{$set}) {
149 0         0 $$self{'err'} = "Set not loaded: $set";
150 0         0 return;
151             }
152              
153 3         4 return @{ $$self{'set'}{$set}{'search'} };
  3         13  
154             }
155              
156 3         4 return @{ $$self{'search'} };
  3         12  
157             }
158              
159 4     4   20 no strict 'refs';
  4         8  
  4         1076  
160             sub _load_lexicon {
161 8     8   17 my($self,$set,$locale) = @_;
162 8 100       28 return if (exists $$self{'mess'}{$set}{$locale});
163              
164 5 50       29 if ($set !~ /^[a-zA-Z0-9_]+$/) {
165 0         0 $$self{'err'} = "Set must be alphanumeric/underscore: $set";
166 0         0 return;
167             }
168              
169 5         180 my $m = "Locale::VersionedMessages::Sets::${set}::${locale}";
170 5         431 eval "require $m";
171 5 50       911 if ($@) {
172 0         0 chomp($@);
173 0         0 $$self{'err'} = "Unable to load lexicon: $set [$locale]: $@";
174 0         0 return;
175             }
176              
177 5         8 $$self{'mess'}{$set}{$locale} = { %{ "${m}::Messages" } };
  5         48  
178              
179 5         10 foreach my $msgid (sort keys %{ $$self{'mess'}{$set}{$locale} }) {
  5         37  
180 24 50       76 if (! exists $$self{'set'}{$set}{'messages'}{$msgid}) {
181 0         0 $$self{'err'} = "Undefined message ID in lexicon: $set [$locale $msgid]";
182 0         0 return;
183             }
184             }
185             }
186 4     4   25 use strict 'refs';
  4         7  
  4         4709  
187              
188             sub message {
189 26     26 1 50508 my($self,$set,$msgid,@args) = @_;
190 26         46 $$self{'err'} = '';
191              
192             # Parse arguments
193              
194 26         60 my($locale,%vals);
195 26 100 66     123 if (@args && @args % 2) {
196 22         564 $locale = shift(@args);
197 22         52 %vals = @args;
198 22 50       76 if (! exists $$self{'set'}{$set}{'all_loc'}{$locale}) {
199 0         0 $$self{'err'} = "Set not defined in locale: $set [ $locale ]";
200 0         0 return '';
201             }
202              
203             } else {
204 4         8 %vals = @args;
205             }
206              
207             # Look up the message.
208              
209 26         31 my @locale;
210 26 100 33     58 if ($locale) {
  4 50 66     30  
    100          
211 22         44 @locale = ($locale);
212              
213             } elsif (exists $$self{'set'}{$set}{'search'} &&
214 4         19 @{ $$self{'set'}{$set}{'search'} }) {
215 0         0 @locale = @{ $$self{'set'}{$set}{'search'} };
  0         0  
216              
217             } elsif (exists $$self{'search'} &&
218             @{ $$self{'search'} }) {
219 3         4 @locale = (@{ $$self{'search'} }, $$self{'set'}{$set}{'def_loc'});
  3         13  
220              
221             } else {
222 1         6 @locale = ($$self{'set'}{$set}{'def_loc'});
223             }
224              
225 26         28 my $message;
226              
227 26         38 foreach my $l (@locale) {
228 27 50       84 next if (! exists $$self{'set'}{$set}{'all_loc'}{$l});
229              
230 27 100       68 if (! exists $$self{'mess'}{$set}{$l}) {
231 4         49 $self->_load_lexicon($set,$l);
232 4 50       552 if ($$self{'err'}) {
233 0 0       0 if (wantarray) {
234 0         0 return ('');
235             } else {
236 0         0 return '';
237             }
238             }
239             }
240              
241 27 100       78 if (exists $$self{'mess'}{$set}{$l}{$msgid}) {
242 24         27 $locale = $l;
243 24         55 $message = $$self{'mess'}{$set}{$l}{$msgid}{'text'};
244 24         35 last;
245             }
246             }
247              
248 26 100       51 if (! $message) {
249 2         7 $$self{'err'} = "Message not found in specified lexicons: $msgid";
250 2         10 return;
251             }
252              
253 24         63 $message = $self->_fix_message($set,$msgid,$message,$locale,%vals);
254              
255 24 100       51 if (wantarray) {
256 3         16 return ($message,$locale);
257             } else {
258 21         91 return $message;
259             }
260             }
261              
262             sub query_msg_locales {
263 3     3 1 418 my($self,$set,$msgid) = @_;
264 3         5 $$self{'err'} = '';
265              
266 3 50       10 if (! exists $$self{'set'}{$set}) {
267 0         0 $$self{'err'} = "Set not loaded: $set";
268 0         0 return ();
269             }
270 3 100       9 if (! exists $$self{'set'}{$set}{'messages'}{$msgid}) {
271 1         4 $$self{'err'} = "Message ID not defined in set: $set [$msgid]";
272 1         4 return ();
273             }
274              
275 2         3 my %all_loc = %{ $$self{'set'}{$set}{'all_loc'} };
  2         10  
276 2         7 my $def_loc = $$self{'set'}{$set}{'def_loc'};
277 2         4 delete $all_loc{$def_loc};
278              
279 2         4 my @locale = ($def_loc);
280 2         8 foreach my $locale (sort keys %all_loc) {
281 4         8 $self->_load_lexicon($set,$locale);
282 4 50       12 return () if ($$self{'err'});
283 4 100       13 if (exists $$self{'mess'}{$set}{$locale}{$msgid}) {
284 3         8 push(@locale,$locale);
285             }
286             }
287              
288 2         10 return @locale;
289             }
290              
291             sub query_msg_vers {
292 6     6 1 568 my($self,$set,$msgid,$locale) = @_;
293 6         9 $$self{'err'} = '';
294              
295 6 50       18 if (! exists $$self{'set'}{$set}) {
296 0         0 $$self{'err'} = "Set not loaded: $set";
297 0         0 return '';
298             }
299 6 50       18 if (! exists $$self{'set'}{$set}{'messages'}{$msgid}) {
300 0         0 $$self{'err'} = "Message ID not defined in set: $set [$msgid]";
301 0         0 return '';
302             }
303              
304 6 100       15 $locale = $$self{'set'}{$set}{'def_loc'} if (! $locale);
305              
306 6 50       17 if (! exists $$self{'set'}{$set}{'all_loc'}{$locale}) {
307 0         0 $$self{'err'} = "Lexicon not available for set: $set [$locale]";
308 0         0 return '';
309             }
310              
311 6 100       21 if (exists $$self{'mess'}{$set}{$locale}{$msgid}) {
312 5         22 return $$self{'mess'}{$set}{$locale}{$msgid}{'vers'};
313             }
314 1         4 return 0;
315             }
316              
317             ########################################################################
318             # MESSAGE SUBSTITUTIONS
319             ########################################################################
320              
321             # This takes a message and performs substitutions for each of
322             # the different substitution values.
323             #
324             sub _fix_message {
325 24     24   65 my($self,$set,$msgid,$message,$locale,%vals) = @_;
326              
327             # No substitutions.
328              
329 24         26 my @vals;
330 24 100       80 if (exists $$self{'set'}{$set}{'messages'}{$msgid}{'vals'}) {
331 21         22 @vals = @{ $$self{'set'}{$set}{'messages'}{$msgid}{'vals'} };
  21         76  
332             }
333 24 100       53 if (! @vals) {
334 4 100       13 if (%vals) {
335 1         4 $$self{'err'} = "Message does not contain substitutions, but " .
336             "values were supplied: $msgid";
337 1         3 return '';
338             }
339 3         7 return $message;
340             }
341              
342             # Check each substitution.
343              
344 20         36 foreach my $val (sort @vals) {
345 20         20 my $done;
346 20 100       38 if (! exists $vals{$val}) {
347 1         5 $$self{'err'} = "A required substitution value was not passed in: " .
348             "$msgid [$val]";
349 1         4 return '';
350             }
351 19         41 ($message,$done) = $self->_substitute($set,$msgid,$locale,
352             $message,$val,$vals{$val});
353 19 100       57 return '' if ($$self{'err'});
354 13 100       25 if (! $done) {
355 1         6 $$self{'err'} = "The message in a lexicon does not contain a required " .
356             "substitution: $msgid [$locale $val]";
357 1         5 return '';
358             }
359 12         31 delete $vals{$val};
360             }
361 12         30 foreach my $val (sort keys %vals) {
362 1         4 $$self{'err'} = "An invalid value was passed in: $msgid [$val]";
363 1         3 return '';
364             }
365              
366 11         29 return $message;
367             }
368              
369             # This does the acutal substitution for a single substitution value.
370             #
371             # If the substitution is found, $done = 1 will be returned.
372             #
373             sub _substitute {
374 19     19   37 my($self,$set,$msgid,$locale,$message,$var,$val) = @_;
375 19         22 my $done = 0;
376              
377             # Simple substitutions: [foo]
378              
379 19 100       184 if ($message =~ s/\[\s*$var\s*\]/$val/sg) {
380 4         6 $done = 1;
381             }
382              
383             # Formatted substitutions: [ foo : FORMAT ]
384              
385 19         63 my $fmt_re = qr/\s*:\s*(%.*?)/;
386              
387 19         172 while ($message =~ s/\[\s*$var$fmt_re\s*\]/__L_M_TMP__/s) {
388 3         8 my $fmt = $1;
389              
390 4     4   23 no warnings;
  4         12  
  4         204  
391 3         11 $val = sprintf($fmt,$val);
392 4     4   18 use warnings;
  4         6  
  4         1441  
393 3 100       9 if ($val eq $fmt) {
394 2         6 $$self{'err'} = "Invalid sprintf format: $msgid [$locale $var]";
395 2         9 return;
396             }
397 1         4 $message =~ s/__L_M_TMP__/$val/s;
398 1         5 $done = 1;
399             }
400              
401             # Quant substitutions: [ foo : quant [ : FORMAT ] ... ]
402              
403 17         48 my ($msg,$d) = $self->_quant($set,$msgid,$locale,$message,$var,$val);
404 17 100       47 return if ($$self{'err'});
405 13         17 $message = $msg;
406 13 100       22 $done = $d if ($d);
407              
408 13         43 return ($message,$done);
409             }
410              
411             # This tests a string for any quant substitutions and returns the
412             # string.
413             #
414             sub _quant {
415 17     17   34 my($self,$set,$msgid,$locale,$mess,$var,$val) = @_;
416              
417 17         19 my $val_orig = $val;
418 17         45 my $fmt_re = qr/\s*:\s*(%.*?)/;
419 17         41 my $brack_re = qr/\s+\[([^]]*?)\]/;
420 17         37 my $sq_re = qr/\s+'([^']*?)'/;
421 17         33 my $dq_re = qr/\s+"([^"]*?)"/;
422 17         33 my $ws_re = qr/\s+(\S*)/;
423 17         133 my $tok_re = qr/(?:$brack_re|$sq_re|$dq_re|$ws_re)/;
424 17         22 my $tmp = '__L_M_TMP__';
425 17         19 my $done = 0;
426              
427             SUBST:
428 17         357 while ($mess =~ s/\[\s*$var\s*:\s*quant\s*(?:$fmt_re)?($tok_re+)\s*\]/$tmp/s) {
429 11         23 my $fmt = $1;
430 11         19 my $tokens= $2;
431              
432 11 50       23 if ($fmt) {
433 4     4   24 no warnings;
  4         7  
  4         126  
434 0         0 $val = sprintf($fmt,$val);
435 4     4   49 use warnings;
  4         18  
  4         3891  
436 0 0       0 if ($val eq $fmt) {
437 0         0 $$self{'err'} = "Invalid sprintf format: $msgid [$locale $var]";
438 0         0 return;
439             }
440             }
441              
442 11         12 my @tok;
443 11         120 while ($tokens =~ s/^$tok_re//) {
444 38   66     280 push(@tok, $1 || $2 || $3 || $4);
445             }
446              
447 11 100       26 if (@tok % 2 == 0) {
448 1         6 $$self{'err'} = "Default string required in quant substitution: " .
449             "$msgid [$locale $var]";
450 1         7 return;
451             }
452              
453             # @tok is (TEST, STRING, TEST, STRING, ..., DEFAULT_STRING)
454              
455 10         20 while (@tok) {
456 15         18 my $ele = shift(@tok);
457              
458             # DEFAULT_STRING
459              
460 15 100       31 if (! @tok) {
461 3         16 $ele =~ s/_$var/$val/g;
462 3         16 $mess =~ s/$tmp/$ele/s;
463 3         4 $done = 1;
464 3         20 next SUBST;
465             }
466              
467             # TEST, STRING
468              
469 12         26 my $flag = $self->_test($set,$msgid,$locale,$var,$val_orig,$ele);
470 12 100       44 return if ($$self{'err'});
471              
472 9         11 $ele = shift(@tok);
473 9 100       22 if ($flag) {
474 4         14 $ele =~ s/_$var/$val/g;
475 4         19 $mess =~ s/$tmp/$ele/s;
476 4         6 $done = 1;
477 4         27 next SUBST;
478             }
479             }
480             }
481              
482 13         75 return($mess,$done);
483             }
484              
485             # This parses a condition string and returns 1 if the condition is true for
486             # this value.
487             #
488             sub _test {
489 12     12   24 my($self,$set,$msgid,$locale,$var,$n,$test) = @_;
490              
491             # $n must be an unsigned integer
492              
493 12 100       43 if ($n !~ /^\d+$/) {
494 1         5 $$self{'err'} = "Quantity test requires an unsigned integer: " .
495             "$msgid [$locale $var]";
496 1         3 return;
497             }
498              
499             # Currently, test can only have:
500             # whitespace
501             # _VAR
502             # ( ) && || < <= == != >= >
503             # DIGITS
504             # in them.
505              
506 11         15 my $tmp = $test;
507 11         111 $tmp =~ s/(?:\s|\d|_$var|[()&|<=!>])//g;
508 11 100       28 if ($tmp) {
509 1         5 $$self{'err'} = "Quantity test contains invalid characters: " .
510             "$msgid [$locale $var]";
511 1         4 return;
512             }
513              
514             # Parse the tests.
515              
516             # 1) _VAR => $n
517              
518 10         92 $test =~ s/\s*_$var\s*/ $n /g;
519              
520             # 2) DIGITS % DIGITS => DIGITS
521              
522 10         37 while ($test =~ s/\s*(\d+)\s*%\s*(\d+)\s*/__L_M_TMP__/) {
523 0         0 my $m = $1 % $2;
524 0         0 $test =~ s/__L_M_TMP__/ $m /;
525             }
526              
527             # 3) DIGITS OP DIGITS => 0 or 1
528              
529 10         51 while ($test =~ s/\s*(\d+)\s*(==|>=|<=|!=|>|<)\s*(\d+)\s*/__L_M_TMP__/) {
530 11         31 my($m,$op,$n) = ($1,$2,$3);
531 11         9 my $x;
532 11 100       34 if ($op eq '==') {
    50          
    50          
    100          
    50          
    0          
533 9 100       20 $x = ($m==$n ? 1 : 0);
534             } elsif ($op eq '!=') {
535 0 0       0 $x = ($m!=$n ? 1 : 0);
536             } elsif ($op eq '>=') {
537 0 0       0 $x = ($m>=$n ? 1 : 0);
538             } elsif ($op eq '<=') {
539 1 50       4 $x = ($m<=$n ? 1 : 0);
540             } elsif ($op eq '>') {
541 1 50       6 $x = ($m>$n ? 1 : 0);
542             } elsif ($op eq '<') {
543 0 0       0 $x = ($m<$n ? 1 : 0);
544             }
545 11         50 $test =~ s/__L_M_TMP__/$x/;
546             }
547              
548             # Repeat until done:
549             # 4) (DIGITS) => DIGITS
550             # 5) DIGITS BOP DIGITS => 0 or 1
551              
552 10         10 while (1) {
553 11         13 my $done = 1;
554 11 100       27 if ($test =~ s/\s*\(\s*(\d+)\s*\)\s*/$1/g) {
555 1         2 $done = 0;
556             }
557 11         25 while ($test =~ s/\s*(\d+)\s*(\|\||&&)\s*(\d+)\s*/__L_M_TMP__/) {
558 1         4 my($m,$op,$n) = ($1,$2,$3);
559 1         1 my $x;
560 1 50       46 if ($op eq '&&') {
    0          
561 1 50 33     13 $x = ($m && $n ? 1 : 0);
562             } elsif ($op eq '||') {
563 0 0 0     0 $x = ($m || $n ? 1 : 0);
564             }
565 1         4 $test =~ s/__L_M_TMP__/$x/;
566 1         4 $done = 0;
567             }
568 11 100       20 last if ($done);
569             }
570              
571             # Final check:
572             # 6) DIGITS => 0 or 1
573              
574 10 100       34 if ($test =~ /^\s*(\d+)\s*$/) {
575 9 100       36 return ($1 ? 1 : 0);
576             }
577              
578 1         4 $$self{'err'} = "Quantity test malformed: $msgid [$locale $var]";
579 1         3 return;
580             }
581              
582             1;
583             # Local Variables:
584             # mode: cperl
585             # indent-tabs-mode: nil
586             # cperl-indent-level: 3
587             # cperl-continued-statement-offset: 2
588             # cperl-continued-brace-offset: 0
589             # cperl-brace-offset: 0
590             # cperl-brace-imaginary-offset: 0
591             # cperl-label-offset: 0
592             # End: